7 Commits

Author SHA1 Message Date
b330fb8141 fix dark mode css
Some checks failed
Angular Build / build (push) Has been cancelled
2026-04-27 23:53:51 +02:00
8b3647b023 dark mode
Some checks failed
Angular Build / build (push) Has been cancelled
2026-04-27 23:42:53 +02:00
5dffcf8cd2 fix css issues 2026-04-27 23:21:19 +02:00
e1aacf2707 RELEASE 2.0
Some checks failed
Angular Build / build (push) Has been cancelled
2026-04-27 23:02:36 +02:00
8c637addf5 fix monitor bg contrast
Some checks failed
Angular Build / build (push) Has been cancelled
2026-04-27 23:01:25 +02:00
e4bbe6e75c fix mobile buttons 2026-04-27 22:34:35 +02:00
30115da841 fix styling 2026-04-27 22:02:15 +02:00
57 changed files with 1243 additions and 682 deletions

View File

@@ -22,7 +22,9 @@
"base": "dist/wgenerator" "base": "dist/wgenerator"
}, },
"index": "src/index.html", "index": "src/index.html",
"polyfills": ["src/polyfills.ts"], "polyfills": [
"src/polyfills.ts"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less", "inlineStyleLanguage": "less",
"assets": [ "assets": [
@@ -38,9 +40,17 @@
"src/assets", "src/assets",
"src/manifest.webmanifest" "src/manifest.webmanifest"
], ],
"styles": ["src/custom-theme.scss", "src/styles/styles.less", "src/styles/shadow.less"], "styles": [
"src/custom-theme.scss",
"src/styles/styles.less",
"src/styles/shadow.less"
],
"scripts": [], "scripts": [],
"allowedCommonJsDependencies": ["lodash", "docx", "qrcode"] "allowedCommonJsDependencies": [
"lodash",
"docx",
"qrcode"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -58,8 +68,8 @@
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "4kB", "maximumWarning": "40kB",
"maximumError": "8kB" "maximumError": "80kB"
} }
], ],
"outputHashing": "all" "outputHashing": "all"
@@ -89,14 +99,19 @@
"options": { "options": {
"runner": "vitest", "runner": "vitest",
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"setupFiles": ["src/test-vitest.ts"], "setupFiles": [
"src/test-vitest.ts"
],
"runnerConfig": true "runnerConfig": true
} }
}, },
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] "lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "wgenerator", "name": "wgenerator",
"version": "1.6", "version": "2.0",
"scripts": { "scripts": {
"start": "ng serve", "start": "ng serve",
"build": "ng build --configuration production", "build": "ng build --configuration production",

View File

@@ -1,7 +1,8 @@
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, Component, OnInit, inject} from '@angular/core';
import {fader} from './animations'; import {fader} from './animations';
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';
import {ThemeService} from './services/theme/theme.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -12,6 +13,10 @@ 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() {
inject(ThemeService);
}
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);

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -36,7 +36,7 @@
filter: blur(10px); filter: blur(10px);
&.visible { &.visible {
opacity: 0.5; opacity: 0.7;
} }
} }
@@ -54,7 +54,7 @@
filter: blur(5px); filter: blur(5px);
&.visible { &.visible {
opacity: 0.4; opacity: 0.6;
} }
} }
@@ -63,7 +63,7 @@
filter: blur(8px); filter: blur(8px);
&.visible { &.visible {
opacity: 0.2; opacity: 0.4;
} }
} }
@@ -72,7 +72,7 @@
filter: blur(8px); filter: blur(8px);
&.visible { &.visible {
opacity: 0.2; opacity: 0.4;
} }
} }

View File

@@ -1,5 +1,10 @@
@if (show) { <app-page-frame title="Präsentation" [withMenu]="false">
<app-card [closeIcon]="faIcon" [heading]="show.showType | showType" [subheading]="show.date.toDate() | date:'dd.MM.yyyy'" closeLink="/presentation/select"> @if (show) {
<app-card [closeIcon]="faIcon"
[heading]="show.showType | showType"
[subheading]="show.date.toDate() | date:'dd.MM.yyyy'"
closeLink="/presentation/select"
content>
@if (!progress) { @if (!progress) {
<div class="song"> <div class="song">
@if (show) { @if (show) {
@@ -44,7 +49,8 @@
{{ song.title }} {{ song.title }}
</div> </div>
</div> </div>
} @if (show) { }
@if (show) {
<div class="song-parts"> <div class="song-parts">
@for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) { @for (section of song.sections; track section.type + '-' + section.number + '-' + $index; let i = $index) {
<div <div
@@ -84,19 +90,19 @@
} }
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Überschrift</mat-label> <mat-label>Überschrift</mat-label>
<input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption" autocomplete="off" id="dynamic-caption" matInput type="text" /> <input (ngModelChange)="onDynamicCaption($event, show.id)" [ngModel]="show.presentationDynamicCaption"
autocomplete="off" id="dynamic-caption" matInput type="text" />
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Text</mat-label> <mat-label>Text</mat-label>
<textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText" autocomplete="off" id="dynamic-text" matInput></textarea> <textarea (ngModelChange)="onDynamicText($event, show.id)" [ngModel]="show.presentationDynamicText"
autocomplete="off" id="dynamic-text" matInput></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
@if (show) { @if (show) {
<div class="div-bottom"> <div class="div-bottom">
<button class="btn-start-presentation" mat-button routerLink="/presentation/monitor"> <app-button [icon]="faDesktop" routerLink="/presentation/monitor">Präsentation starten</app-button>
<fa-icon [icon]="faDesktop"></fa-icon>
Präsentation starten
</button>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Hintergrund</mat-label> <mat-label>Hintergrund</mat-label>
<mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground"> <mat-select (ngModelChange)="onBackground($event, show.id)" [ngModel]="show.presentationBackground">
@@ -112,8 +118,11 @@
><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb /> ><input (ngModelChange)="onZoom($event, show.id)" [ngModel]="show.presentationZoom" matSliderThumb />
</mat-slider> </mat-slider>
</div> </div>
} @if (show) { }
@if (show) {
<app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song> <app-add-song [addedLive]="true" [showSongs]="showSongs" [show]="show" [songs]="songs$|async"></app-add-song>
} } }
</app-card> }
} </app-card>
}
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, inject} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy} from '@angular/core';
import {combineLatest, Subject} from 'rxjs'; import {combineLatest, Subject} from 'rxjs';
import {PresentationBackground, Show} from '../../shows/services/show'; import {PresentationBackground, Show} from '../../shows/services/show';
import {ShowSongService} from '../../shows/services/show-song.service'; import {ShowSongService} from '../../shows/services/show-song.service';
@@ -17,15 +17,15 @@ import {CardComponent} from '../../../widget-modules/components/card/card.compon
import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input'; import {MatInput} from '@angular/material/input';
import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButton} from '@angular/material/button';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {MatSelect} from '@angular/material/select'; import {MatSelect} from '@angular/material/select';
import {MatOption} from '@angular/material/core'; import {MatOption} from '@angular/material/core';
import {MatSlider, MatSliderThumb} from '@angular/material/slider'; import {MatSlider, MatSliderThumb} from '@angular/material/slider';
import {AddSongComponent} from '../../../widget-modules/components/add-song/add-song.component'; import {AddSongComponent} from '../../../widget-modules/components/add-song/add-song.component';
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 {SectionTypePipe} from '../../../widget-modules/pipes/section-type-translator/section-type.pipe'; import {SectionTypePipe} from '../../../widget-modules/pipes/section-type-translator/section-type.pipe';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
export interface PresentationSong { export interface PresentationSong {
id: string; id: string;
@@ -46,9 +46,7 @@ export interface PresentationSong {
MatInput, MatInput,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
MatButton,
RouterLink, RouterLink,
FaIconComponent,
MatSelect, MatSelect,
MatOption, MatOption,
MatSlider, MatSlider,
@@ -58,6 +56,8 @@ export interface PresentationSong {
DatePipe, DatePipe,
ShowTypePipe, ShowTypePipe,
SectionTypePipe, SectionTypePipe,
PageFrameComponent,
ButtonComponent,
], ],
}) })
export class RemoteComponent implements OnDestroy { export class RemoteComponent implements OnDestroy {
@@ -87,12 +87,12 @@ export class RemoteComponent implements OnDestroy {
map(_ => _.currentShow), map(_ => _.currentShow),
filter((showId): showId is string => !!showId), filter((showId): showId is string => !!showId),
distinctUntilChanged(), distinctUntilChanged(),
takeUntil(this.destroy$) takeUntil(this.destroy$),
); );
const show$ = currentShowId$.pipe( const show$ = currentShowId$.pipe(
switchMap(showId => this.showService.read$(showId)), switchMap(showId => this.showService.read$(showId)),
takeUntil(this.destroy$) takeUntil(this.destroy$),
); );
const parsedSongs$ = currentShowId$.pipe( const parsedSongs$ = currentShowId$.pipe(
@@ -105,7 +105,7 @@ export class RemoteComponent implements OnDestroy {
sections: this.textRenderingService.parse(song.text, null, false), sections: this.textRenderingService.parse(song.text, null, false),
})), })),
})), })),
takeUntil(this.destroy$) takeUntil(this.destroy$),
); );
combineLatest([show$, parsedSongs$]) combineLatest([show$, parsedSongs$])

View File

@@ -1,20 +1,24 @@
@if (shows$ | async; as shows) { <app-page-frame title="Präsentation" [withMenu]="false">
<div @fade> @if (shows$ | async; as shows) {
<div @fade content>
@if (visible) { @if (visible) {
<app-card heading="Bitte eine Veranstaltung auswählen"> <app-card heading="Bitte eine Veranstaltung auswählen">
@if (!shows.length) { @if (!shows.length) {
<p>Es ist derzeit keine Veranstaltung vorhanden</p> <p>Es ist derzeit keine Veranstaltung vorhanden</p>
} @if (shows.length>0) { }
@if (shows.length > 0) {
<div class="list"> <div class="list">
@for (show of shows; track show.id) { @for (show of shows; track show.id) {
<button (click)="selectShow(show)" mat-stroked-button> <app-button (click)="selectShow(show)" class="full-width">
<app-user-name [userId]="show.owner"></app-user-name> <app-user-name [userId]="show.owner"></app-user-name>
, {{ show.showType | showType }}, {{ show.date.toDate() | date: "dd.MM.yyyy" }} , {{ show.showType | showType }}, {{ show.date.toDate() | date: "dd.MM.yyyy" }}
</button> </app-button>
} }
</div> </div>
} }
</app-card> </app-card>
} }
</div> </div>
} }
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {ShowService} from '../../shows/services/show.service'; import {ShowService} from '../../shows/services/show.service';
import {Show} from '../../shows/services/show'; import {Show} from '../../shows/services/show';
@@ -7,16 +7,17 @@ import {Router} from '@angular/router';
import {fade} from '../../../animations'; import {fade} from '../../../animations';
import {AsyncPipe, DatePipe} from '@angular/common'; import {AsyncPipe, DatePipe} from '@angular/common';
import {CardComponent} from '../../../widget-modules/components/card/card.component'; import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {MatButton} from '@angular/material/button';
import {UserNameComponent} from '../../../services/user/user-name/user-name.component'; import {UserNameComponent} from '../../../services/user/user-name/user-name.component';
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 {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
@Component({ @Component({
selector: 'app-select', selector: 'app-select',
templateUrl: './select.component.html', templateUrl: './select.component.html',
styleUrls: ['./select.component.less'], styleUrls: ['./select.component.less'],
animations: [fade], animations: [fade],
imports: [CardComponent, MatButton, UserNameComponent, AsyncPipe, DatePipe, ShowTypePipe], imports: [CardComponent, UserNameComponent, AsyncPipe, DatePipe, ShowTypePipe, PageFrameComponent, ButtonComponent],
}) })
export class SelectComponent implements OnInit { export class SelectComponent implements OnInit {
private showService = inject(ShowService); private showService = inject(ShowService);

View File

@@ -1,5 +1,6 @@
<div mat-dialog-content> <div mat-dialog-content>
<p>Bitte melde die in dieser Veranstaltung verwendeten CCLI-Titel. Die Meldung ist Teil der CCLI-Lizenz und sorgt dafür, dass Songwriter und Verlage korrekt vergütet werden.</p> <p>Bitte melde die in dieser Veranstaltung verwendeten CCLI-Titel. Die Meldung ist Teil der CCLI-Lizenz und sorgt
dafür, dass Songwriter und Verlage korrekt vergütet werden.</p>
<p> <p>
Die Meldung erfolgt über Die Meldung erfolgt über
<a [href]="reportingUrl" rel="noreferrer" target="_blank">{{ reportingUrl }}</a>. <a [href]="reportingUrl" rel="noreferrer" target="_blank">{{ reportingUrl }}</a>.
@@ -34,7 +35,7 @@
} }
</div> </div>
</div> </div>
<div mat-dialog-actions> <app-button-row>
<button [mat-dialog-close]="false" mat-button>Abbrechen</button> <app-button (click)="dialogRef.close(false)" [icon]="faClose">Abbrechen</app-button>
<button [mat-dialog-close]="true" cdkFocusInitial mat-button>Alle CCLI-Titel wurden gemeldet</button> <app-button (click)="dialogRef.close(true)" [icon]="faReport">Alle CCLI-Titel wurden gemeldet</app-button>
</div> </app-button-row>

View File

@@ -72,3 +72,7 @@
justify-content: flex-start; justify-content: flex-start;
} }
} }
app-button-row {
margin: 0 var(--gap-l) var(--gap-l);
}

View File

@@ -1,8 +1,9 @@
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, MatDialogContent, MatDialogRef} from '@angular/material/dialog';
import {MatButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faArrowUpRightFromSquare, faCheck} from '@fortawesome/free-solid-svg-icons'; import {faArrowUpRightFromSquare, faCheck, faCompactDisc, faXmark} from '@fortawesome/free-solid-svg-icons';
import {ButtonRowComponent} from '../../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../../widget-modules/components/button/button.component';
export interface ReportDialogSong { export interface ReportDialogSong {
title: string; title: string;
@@ -15,7 +16,7 @@ export interface ReportDialogData {
@Component({ @Component({
selector: 'app-report-dialog', selector: 'app-report-dialog',
imports: [MatButton, MatDialogActions, MatDialogContent, MatDialogClose, FaIconComponent], imports: [MatDialogContent, FaIconComponent, ButtonRowComponent, ButtonComponent],
templateUrl: './report-dialog.component.html', templateUrl: './report-dialog.component.html',
styleUrl: './report-dialog.component.less', styleUrl: './report-dialog.component.less',
standalone: true, standalone: true,
@@ -25,6 +26,11 @@ export class ReportDialogComponent {
public readonly faOpen = faArrowUpRightFromSquare; public readonly faOpen = faArrowUpRightFromSquare;
public readonly faCheck = faCheck; public readonly faCheck = faCheck;
public data = inject<ReportDialogData>(MAT_DIALOG_DATA); public data = inject<ReportDialogData>(MAT_DIALOG_DATA);
public dialogRef = inject(MatDialogRef<ReportDialogComponent>);
public faReport = faCompactDisc;
public faClose = faXmark;
private readonly openedNumbers = new Set<string>(); private readonly openedNumbers = new Set<string>();
public getSongReportingUrl(ccliNumber: string): string { public getSongReportingUrl(ccliNumber: string): string {

View File

@@ -1,4 +1,5 @@
<div> <app-page-frame title="Veranstaltungen" [withMenu]="false">
<div content>
<app-card [closeLink]="'/shows/'+form.value.id" heading="Veranstaltung ändern"> <app-card [closeLink]="'/shows/'+form.value.id" heading="Veranstaltung ändern">
<div [formGroup]="form" class="split"> <div [formGroup]="form" class="split">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -6,12 +7,12 @@
<mat-select formControlName="showType"> <mat-select formControlName="showType">
<mat-optgroup label="öffentlich"> <mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) { @for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option> <mat-option [value]="key">{{ key | showType }}</mat-option>
} }
</mat-optgroup> </mat-optgroup>
<mat-optgroup label="privat"> <mat-optgroup label="privat">
@for (key of showTypePrivate; track key) { @for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option> <mat-option [value]="key">{{ key | showType }}</mat-option>
} }
</mat-optgroup> </mat-optgroup>
</mat-select> </mat-select>
@@ -28,4 +29,5 @@
<app-button (click)="onSave()" [icon]="faSave">Speichern</app-button> <app-button (click)="onSave()" [icon]="faSave">Speichern</app-button>
</app-button-row> </app-button-row>
</app-card> </app-card>
</div> </div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {ShowDataService} from '../services/show-data.service'; import {ShowDataService} from '../services/show-data.service';
import {Observable, take} from 'rxjs'; import {Observable, take} from 'rxjs';
import {Show} from '../services/show'; import {Show} from '../services/show';
@@ -18,6 +18,7 @@ import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/m
import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component'; import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component'; import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
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 {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-edit', selector: 'app-edit',
@@ -39,6 +40,7 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
ButtonRowComponent, ButtonRowComponent,
ButtonComponent, ButtonComponent,
ShowTypePipe, ShowTypePipe,
PageFrameComponent,
], ],
}) })
export class EditComponent implements OnInit { export class EditComponent implements OnInit {
@@ -70,7 +72,7 @@ export class EditComponent implements OnInit {
map(param => param as {showId: string}), map(param => param as {showId: string}),
map(param => param.showId), map(param => param.showId),
switchMap((showId: string) => this.showService.read$(showId)), switchMap((showId: string) => this.showService.read$(showId)),
take(1) take(1),
) )
.subscribe(show => { .subscribe(show => {
this.form.setValue({ this.form.setValue({

View File

@@ -1,14 +1,14 @@
@if (show) { @if (show) {
<div class="list-item"> <div class="list-item">
<div>{{ show.date.toDate() | date: "dd.MM.yyyy" }}</div> <div class="date">{{ show.date.toDate() | date: "dd.MM.yyyy" }}</div>
<div> <div class="user">
<app-user-name [userId]="show.owner"></app-user-name> <app-user-name [userId]="show.owner"></app-user-name>
</div> </div>
<div>{{ show.showType | showType }}</div> <div class="show-type">{{ show.showType | showType }}</div>
<div> <div class="badge">
@if (showStatusBadge) { @if (showStatusBadge) {
<app-badge [type]="showStatusBadgeType">{{ showStatusBadge }}</app-badge> <app-badge [type]="showStatusBadgeType">{{ showStatusBadge }}</app-badge>
} }
</div> </div>
</div> </div>
} }

View File

@@ -2,6 +2,15 @@
padding: 5px 20px; padding: 5px 20px;
display: grid; display: grid;
grid-template-columns: 100px 150px auto 160px; grid-template-columns: 100px 150px auto 160px;
grid-template-areas: "date user show-type badge";
@media screen and (max-width: 860px) {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto auto;
grid-template-areas: "date user badge" ". show-type badge";
row-gap: 3px;
}
min-height: 21px; min-height: 21px;
& > div { & > div {
@@ -16,6 +25,23 @@
background: var(--hover-background); background: var(--hover-background);
color: var(--text); color: var(--text);
} }
.date {
grid-area: date;
}
.user {
grid-area: user;
}
.show-type {
grid-area: show-type;
}
.badge {
grid-area: badge;
justify-content: end;
}
} }
.number { .number {

View File

@@ -1,5 +1,5 @@
@if (showSidebar$ | async) { @if (showSidebar$ | async) {
<app-sidebar> <app-page-frame title="Veranstaltungen">
<div class="sidebar-content" sidebar> <div class="sidebar-content" sidebar>
<app-filter [shows]="(publicShows$ | async) ?? []"></app-filter> <app-filter [shows]="(publicShows$ | async) ?? []"></app-filter>
</div> </div>
@@ -15,26 +15,31 @@
></app-list-item> ></app-list-item>
} }
<div *appRole="['leader']" class="list-action"> <div *appRole="['leader']" class="list-action">
<app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen </app-button> <app-button [fullWidth]="true" [icon]="faNewShow" routerLink="new">Neue Veranstaltung anlegen</app-button>
</div> </div>
</app-card> </app-card>
} @if (publicShows$ | async; as shows) { @if (shows.length > 0) { }
@if (publicShows$ | async; as shows) {
@if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen"> <app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) { @for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item> <app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
} }
</app-card> </app-card>
} } }
}
</div> </div>
</app-sidebar> </app-page-frame>
} @else { } @else {
<div> <div>
@if (publicShows$ | async; as shows) { @if (shows.length > 0) { @if (publicShows$ | async; as shows) {
@if (shows.length > 0) {
<app-card [padding]="false" heading="Veröffentlichte Veranstaltungen"> <app-card [padding]="false" heading="Veröffentlichte Veranstaltungen">
@for (show of shows; track trackBy($index, show)) { @for (show of shows; track trackBy($index, show)) {
<app-list-item [routerLink]="show.id" [show]="show"></app-list-item> <app-list-item [routerLink]="show.id" [show]="show"></app-list-item>
} }
</app-card> </app-card>
} } }
</div> }
</div>
} }

View File

@@ -1,3 +1,7 @@
.sidebar-content { .sidebar-content {
padding: 20px; padding: 20px;
} }
.list-action {
margin: 10px 20px;
}

View File

@@ -12,7 +12,7 @@ import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component'; import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {ListItemComponent} from './list-item/list-item.component'; import {ListItemComponent} from './list-item/list-item.component';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component'; import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component'; import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
import {faPlus} from '@fortawesome/free-solid-svg-icons'; import {faPlus} from '@fortawesome/free-solid-svg-icons';
import {RoleDirective} from '../../../services/user/role.directive'; import {RoleDirective} from '../../../services/user/role.directive';
@@ -22,21 +22,33 @@ import {RoleDirective} from '../../../services/user/role.directive';
templateUrl: './list.component.html', templateUrl: './list.component.html',
styleUrls: ['./list.component.less'], styleUrls: ['./list.component.less'],
animations: [fade], animations: [fade],
imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, SidebarComponent, ButtonComponent, RoleDirective], imports: [FilterComponent, CardComponent, ListItemComponent, RouterLink, AsyncPipe, PageFrameComponent, ButtonComponent, RoleDirective],
}) })
export class ListComponent { export class ListComponent {
public faNewShow = faPlus; public faNewShow = faPlus;
private showService = inject(ShowService);
private filterStore = inject(FilterStoreService); private filterStore = inject(FilterStoreService);
private userService = inject(UserService);
public filter$ = this.filterStore.showFilter$; public filter$ = this.filterStore.showFilter$;
public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1)); public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner)); public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType)); public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType));
public archived$ = this.filter$.pipe(map((filterValues: FilterValues) => !!filterValues.archived)); public archived$ = this.filter$.pipe(map((filterValues: FilterValues) => !!filterValues.archived));
private showService = inject(ShowService);
public shows$ = this.showService.list$(); public shows$ = this.showService.list$();
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
map(([shows, lastMonths]) => {
return shows.filter(show => show.published && !show.archived).filter(show => this.matchesTimeFilter(show, lastMonths));
}),
);
public ownShows$ = this.showService.list$(false, true); public ownShows$ = this.showService.list$(false, true);
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe(
map(([queriedShows, fallbackShows, owner, showType]) => {
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
return this.sortShowsByDateDesc(shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType));
}),
);
private userService = inject(UserService);
public privateShows$ = combineLatest([this.ownShows$, this.userService.user$, this.archived$]).pipe( public privateShows$ = combineLatest([this.ownShows$, this.userService.user$, this.archived$]).pipe(
map(([shows, user, showArchived]) => map(([shows, user, showArchived]) =>
shows.filter(show => { shows.filter(show => {
@@ -49,22 +61,9 @@ export class ListComponent {
} }
return !show.published || show.reportedType === 'pending'; return !show.published || show.reportedType === 'pending';
}) }),
), ),
map(shows => this.sortShowsByDateDesc(shows)) map(shows => this.sortShowsByDateDesc(shows)),
);
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
map(([shows, lastMonths]) => {
return shows.filter(show => show.published && !show.archived).filter(show => this.matchesTimeFilter(show, lastMonths));
})
);
public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe(
map(([queriedShows, fallbackShows, owner, showType]) => {
const shows = queriedShows.length > 0 || fallbackShows.length === 0 ? queriedShows : fallbackShows;
return this.sortShowsByDateDesc(shows.filter(show => !owner || show.owner === owner).filter(show => !showType || show.showType === showType));
})
); );
public showSidebar$ = this.userService.user$.pipe(map(user => this.hasSidebarAccess(user?.role))); public showSidebar$ = this.userService.user$.pipe(map(user => this.hasSidebarAccess(user?.role)));

View File

@@ -1,4 +1,5 @@
<div> <app-page-frame title="Veranstaltungen" [withMenu]="false">
<div content>
<app-card closeLink="/shows" heading="Neue Veranstaltung"> <app-card closeLink="/shows" heading="Neue Veranstaltung">
<div [formGroup]="form" class="split"> <div [formGroup]="form" class="split">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@@ -6,12 +7,12 @@
<mat-select formControlName="showType"> <mat-select formControlName="showType">
<mat-optgroup label="öffentlich"> <mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) { @for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option> <mat-option [value]="key">{{ key | showType }}</mat-option>
} }
</mat-optgroup> </mat-optgroup>
<mat-optgroup label="privat"> <mat-optgroup label="privat">
@for (key of showTypePrivate; track key) { @for (key of showTypePrivate; track key) {
<mat-option [value]="key">{{ key | showType }} </mat-option> <mat-option [value]="key">{{ key | showType }}</mat-option>
} }
</mat-optgroup> </mat-optgroup>
</mat-select> </mat-select>
@@ -28,4 +29,5 @@
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button> <app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row> </app-button-row>
</app-card> </app-card>
</div> </div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {ShowDataService} from '../services/show-data.service'; import {ShowDataService} from '../services/show-data.service';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {Show} from '../services/show'; import {Show} from '../services/show';
@@ -16,6 +16,7 @@ import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/m
import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component'; import {ButtonRowComponent} from '../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component'; import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
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 {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-new', selector: 'app-new',
@@ -37,6 +38,7 @@ import {ShowTypePipe} from '../../../widget-modules/pipes/show-type-translater/s
ButtonRowComponent, ButtonRowComponent,
ButtonComponent, ButtonComponent,
ShowTypePipe, ShowTypePipe,
PageFrameComponent,
], ],
}) })
export class NewComponent implements OnInit { export class NewComponent implements OnInit {

View File

@@ -1,5 +1,6 @@
@if (show$ | async; as show) { <app-page-frame title="Veranstaltungen" [withMenu]="false">
<div> @if (show$ | async; as show) {
<div content>
<app-card <app-card
[fullscreen]="useSwiper" [fullscreen]="useSwiper"
closeLink="../" closeLink="../"
@@ -26,8 +27,10 @@
} }
</div> </div>
<div [class.floating]="useSwiper"> <div [class.floating]="useSwiper">
<app-menu-button (click)="onZoomOut()" @fade [icon]="faZoomOut" class="btn-delete btn-icon" matTooltip="Verkleinern"></app-menu-button> <app-menu-button (click)="onZoomOut()" @fade [icon]="faZoomOut" class="btn-delete btn-icon"
<app-menu-button (click)="onZoomIn()" @fade [icon]="faZoomIn" class="btn-delete btn-icon" matTooltip="Vergrößern"></app-menu-button> matTooltip="Verkleinern"></app-menu-button>
<app-menu-button (click)="onZoomIn()" @fade [icon]="faZoomIn" class="btn-delete btn-icon"
matTooltip="Vergrößern"></app-menu-button>
<app-menu-button <app-menu-button
(click)="useSwiper=!useSwiper;fullscreen(useSwiper)" (click)="useSwiper=!useSwiper;fullscreen(useSwiper)"
@fade @fade
@@ -60,11 +63,13 @@
</div> </div>
} }
</div> </div>
} @if (useSwiper) { }
@if (useSwiper) {
<swiper-container scrollbar="true"> <swiper-container scrollbar="true">
@for (song of orderedShowSongs(show); track trackBy(i, song); let i = $index) { @for (song of orderedShowSongs(show); track trackBy(i, song); let i = $index) {
<swiper-slide [style.font-size]="textSize + 'em'" class="song-swipe"> <swiper-slide [style.font-size]="textSize + 'em'" class="song-swipe">
<app-song [fullscreen]="true" [index]="i" [showId]="showId" [showSong]="song" [showText]="true" [show]="show"></app-song> <app-song [fullscreen]="true" [index]="i" [showId]="showId" [showSong]="song" [showText]="true"
[show]="show"></app-song>
<div class="time">{{ currentTime | date: 'HH:mm' }}</div> <div class="time">{{ currentTime | date: 'HH:mm' }}</div>
@if (getNextSong(orderedShowSongs(show), i); as next) { @if (getNextSong(orderedShowSongs(show), i); as next) {
<div class="next-song"> <div class="next-song">
@@ -75,25 +80,36 @@
</swiper-slide> </swiper-slide>
} }
</swiper-container> </swiper-container>
} @if (songs$ | async; as songs) { @if (songs && !show.published && !useSwiper) { }
@if (songs$ | async; as songs) {
@if (songs && !show.published && !useSwiper) {
<app-add-song [showSongs]="showSongs" [show]="show" [songs]="songs"></app-add-song> <app-add-song [showSongs]="showSongs" [show]="show" [songs]="songs"></app-add-song>
} } @if (!useSwiper) { }
}
@if (!useSwiper) {
<app-button-row> <app-button-row>
<ng-container *appRole="['leader']"> <ng-container *appRole="['leader']">
<ng-container *appOwner="show.owner"> <ng-container *appOwner="show.owner">
@if (!show.archived) { @if (!show.archived) {
<app-button (click)="onArchive(true)" [icon]="faBox"> Archivieren</app-button> <app-button (click)="onArchive(true)" [icon]="faBox"> Archivieren</app-button>
} @if (show.archived) { }
@if (show.archived) {
<app-button (click)="onArchive(false)" [icon]="faBoxOpen"> Wiederherstellen</app-button> <app-button (click)="onArchive(false)" [icon]="faBoxOpen"> Wiederherstellen</app-button>
} @if (!show.published) { }
@if (!show.published) {
<app-button (click)="onPublish(show, true)" [icon]="faPublish"> Veröffentlichen</app-button> <app-button (click)="onPublish(show, true)" [icon]="faPublish"> Veröffentlichen</app-button>
} @if (show.published) { }
<app-button (click)="onPublish(show, false)" [icon]="faUnpublish"> Veröffentlichung zurückziehen </app-button> @if (show.published) {
} @if (show.published) { <app-button (click)="onPublish(show, false)" [icon]="faUnpublish"> Veröffentlichung zurückziehen
</app-button>
}
@if (show.published) {
<app-button (click)="onShare(show)" [icon]="faShare"> Teilen</app-button> <app-button (click)="onShare(show)" [icon]="faShare"> Teilen</app-button>
} @if (show.published && show.reportedType === 'pending') { }
@if (show.published && show.reportedType === 'pending') {
<app-button (click)="onReport(show)" [icon]="faReport"> CCLI</app-button> <app-button (click)="onReport(show)" [icon]="faReport"> CCLI</app-button>
} @if (!show.published) { }
@if (!show.published) {
<app-button (click)="onChange(show.id)" [icon]="faSliders"> Ändern</app-button> <app-button (click)="onChange(show.id)" [icon]="faSliders"> Ändern</app-button>
} }
</ng-container> </ng-container>
@@ -106,5 +122,6 @@
</app-button-row> </app-button-row>
} }
</app-card> </app-card>
</div> </div>
} }
</app-page-frame>

View File

@@ -1,6 +1,6 @@
:host { :host {
--button-padding-mobile: 10px; --button-padding-mobile: 4px;
--button-font-size-mobile: 25px; --button-font-size-mobile: 20px;
} }
.song-row:not(:last-child) { .song-row:not(:last-child) {

View File

@@ -1,4 +1,12 @@
import {ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, HostListener, inject, OnDestroy, OnInit} from '@angular/core'; import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
HostListener,
inject,
OnDestroy,
OnInit,
} from '@angular/core';
import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators'; import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {ShowService} from '../services/show.service'; import {ShowService} from '../services/show.service';
@@ -55,6 +63,7 @@ import {BadgeComponent, BadgeType} from '../../../widget-modules/components/badg
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'; import {ensureSwiperElement} from '../../../services/swiper-element';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-show', selector: 'app-show',
@@ -87,6 +96,7 @@ import {ensureSwiperElement} from '../../../services/swiper-element';
ReportedTypePipe, ReportedTypePipe,
PublishedTypePipe, PublishedTypePipe,
BadgeComponent, BadgeComponent,
PageFrameComponent,
], ],
}) })
export class ShowComponent implements OnInit, OnDestroy { export class ShowComponent implements OnInit, OnDestroy {
@@ -140,7 +150,7 @@ export class ShowComponent implements OnInit, OnDestroy {
shareReplay({ shareReplay({
bufferSize: 1, bufferSize: 1,
refCount: true, refCount: true,
}) }),
); );
this.subs.push( this.subs.push(
this.activatedRoute.params this.activatedRoute.params
@@ -148,12 +158,12 @@ export class ShowComponent implements OnInit, OnDestroy {
map(param => param as {showId: string}), map(param => param as {showId: string}),
map(param => param.showId), map(param => param.showId),
switchMap(showId => this.showSongService.list$(showId)), switchMap(showId => this.showSongService.list$(showId)),
filter(_ => !!_ && _.length > 0) filter(_ => !!_ && _.length > 0),
) )
.subscribe(_ => { .subscribe(_ => {
this.showSongs = _; this.showSongs = _;
this.cRef.markForCheck(); this.cRef.markForCheck();
}) }),
); );
this.songs$ = this.show$.pipe( this.songs$ = this.show$.pipe(
@@ -161,7 +171,7 @@ export class ShowComponent implements OnInit, OnDestroy {
shareReplay({ shareReplay({
bufferSize: 1, bufferSize: 1,
refCount: true, refCount: true,
}) }),
); );
} }

View File

@@ -18,7 +18,7 @@
grid-template-areas: "keys title edit delete"; grid-template-areas: "keys title edit delete";
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
grid-template-columns: var(--song-key-column-width, 30px) auto 45px 45px; grid-template-columns: var(--song-key-column-width, 30px) auto 30px 30px;
} }
&.with-drag { &.with-drag {
@@ -26,7 +26,7 @@
grid-template-areas: "drag keys title edit delete"; grid-template-areas: "drag keys title edit delete";
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
grid-template-columns: 24px var(--song-key-column-width, 30px) auto 45px 45px; grid-template-columns: 24px var(--song-key-column-width, 30px) auto 30px 30px;
} }
} }
} }
@@ -36,7 +36,7 @@
grid-template-areas: "title keys edit delete"; grid-template-areas: "title keys edit delete";
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
grid-template-columns: auto var(--song-key-column-width, 30px) 45px 45px; grid-template-columns: auto var(--song-key-column-width, 30px) 30px 30px;
} }
&.with-drag { &.with-drag {
@@ -44,7 +44,7 @@
grid-template-areas: "drag title keys edit delete"; grid-template-areas: "drag title keys edit delete";
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
grid-template-columns: 24px auto var(--song-key-column-width, 30px) 45px 45px; grid-template-columns: 24px auto var(--song-key-column-width, 30px) 30px 30px;
} }
} }
} }
@@ -176,4 +176,5 @@ button {
textarea.edit { textarea.edit {
font-family: 'Ubuntu Mono', monospace; font-family: 'Ubuntu Mono', monospace;
line-height: 15px;
} }

View File

@@ -1,6 +1,6 @@
@if (songs$ | async; as songs) { @if (songs$ | async; as songs) {
<app-sidebar> <app-page-frame title="Lieder">
<div sidebar class="sidebar-content"> <div class="sidebar-content" sidebar>
<app-filter [songs]="songs"></app-filter> <app-filter [songs]="songs"></app-filter>
</div> </div>
<div content> <div content>
@@ -20,11 +20,13 @@
<div class="warning"> <div class="warning">
<fa-icon [icon]="faDraft"></fa-icon> <fa-icon [icon]="faDraft"></fa-icon>
</div> </div>
} @if (song.status === 'set') { }
@if (song.status === 'set') {
<div class="neutral"> <div class="neutral">
<fa-icon [icon]="faDraft"></fa-icon> <fa-icon [icon]="faDraft"></fa-icon>
</div> </div>
} @if (song.status === 'final') { }
@if (song.status === 'final') {
<div class="success"> <div class="success">
<fa-icon [icon]="faFinal"></fa-icon> <fa-icon [icon]="faFinal"></fa-icon>
</div> </div>
@@ -44,5 +46,5 @@
</div> </div>
</app-card> </app-card>
</div> </div>
</app-sidebar> </app-page-frame>
} }

View File

@@ -55,3 +55,7 @@
.success { .success {
color: var(--success); color: var(--success);
} }
.list-action {
margin: var(--gap-m) var(--gap-l);
}

View File

@@ -14,7 +14,7 @@ import {FilterComponent} from './filter/filter.component';
import {CardComponent} from '../../../widget-modules/components/card/card.component'; import {CardComponent} from '../../../widget-modules/components/card/card.component';
import {RoleDirective} from '../../../services/user/role.directive'; import {RoleDirective} from '../../../services/user/role.directive';
import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {SidebarComponent} from '../../../widget-modules/components/sidebar/sidebar.component'; import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component'; import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
interface SongListItem extends Song { interface SongListItem extends Song {
@@ -27,7 +27,7 @@ interface SongListItem extends Song {
styleUrls: ['./song-list.component.less'], styleUrls: ['./song-list.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fade], animations: [fade],
imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, SidebarComponent, ButtonComponent], imports: [FilterComponent, CardComponent, RouterLink, RoleDirective, FaIconComponent, AsyncPipe, PageFrameComponent, ButtonComponent],
}) })
export class SongListComponent { export class SongListComponent {
public faLegal = faBalanceScaleRight; public faLegal = faBalanceScaleRight;
@@ -48,7 +48,7 @@ export class SongListComponent {
...song, ...song,
hasChordValidationIssues: this.textRenderingService.validateChordNotation(song.text ?? '').length > 0, hasChordValidationIssues: this.textRenderingService.validateChordNotation(song.text ?? '').length > 0,
})); }));
}) }),
); );
public trackBy = (index: number, show: SongListItem) => show.id; public trackBy = (index: number, show: SongListItem) => show.id;

View File

@@ -24,6 +24,7 @@
textarea { textarea {
font-family: 'Ubuntu Mono', monospace; font-family: 'Ubuntu Mono', monospace;
line-height: 15px;
} }
} }

View File

@@ -1,5 +1,7 @@
<div> <app-page-frame title="Lieder" [withMenu]="false">
<div content>
<app-edit-song></app-edit-song> <app-edit-song></app-edit-song>
<app-edit-file></app-edit-file> <app-edit-file></app-edit-file>
<app-history></app-history> <app-history></app-history>
</div> </div>
</app-page-frame>

View File

@@ -2,12 +2,13 @@ import {Component, ViewChild} from '@angular/core';
import {EditSongComponent} from './edit-song/edit-song.component'; import {EditSongComponent} from './edit-song/edit-song.component';
import {EditFileComponent} from './edit-file/edit-file.component'; import {EditFileComponent} from './edit-file/edit-file.component';
import {HistoryComponent} from './history/history.component'; import {HistoryComponent} from './history/history.component';
import {PageFrameComponent} from '../../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-edit', selector: 'app-edit',
templateUrl: './edit.component.html', templateUrl: './edit.component.html',
styleUrls: ['./edit.component.less'], styleUrls: ['./edit.component.less'],
imports: [EditSongComponent, EditFileComponent, HistoryComponent], imports: [EditSongComponent, EditFileComponent, HistoryComponent, PageFrameComponent],
}) })
export class EditComponent { export class EditComponent {
@ViewChild(EditSongComponent) public editSongComponent: EditSongComponent | null = null; @ViewChild(EditSongComponent) public editSongComponent: EditSongComponent | null = null;

View File

@@ -1,4 +1,5 @@
<app-card closeLink="../" heading="Neues Lied"> <app-page-frame title="Lieder" [withMenu]="false">
<app-card closeLink="../" heading="Neues Lied" content>
<div [formGroup]="form" class="split"> <div [formGroup]="form" class="split">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Nummer</mat-label> <mat-label>Nummer</mat-label>
@@ -13,4 +14,5 @@
<app-button-row> <app-button-row>
<app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button> <app-button (click)="onSave()" [icon]="faSave">Anlegen</app-button>
</app-button-row> </app-button-row>
</app-card> </app-card>
</app-page-frame>

View File

@@ -1,6 +1,5 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, DestroyRef, inject, OnInit} from '@angular/core';
import {faSave} from '@fortawesome/free-solid-svg-icons'; import {faSave} from '@fortawesome/free-solid-svg-icons';
import {DestroyRef} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {SongService} from '../../services/song.service'; import {SongService} from '../../services/song.service';
@@ -12,12 +11,13 @@ import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input'; import {MatInput} from '@angular/material/input';
import {ButtonRowComponent} from '../../../../widget-modules/components/button-row/button-row.component'; import {ButtonRowComponent} from '../../../../widget-modules/components/button-row/button-row.component';
import {ButtonComponent} from '../../../../widget-modules/components/button/button.component'; import {ButtonComponent} from '../../../../widget-modules/components/button/button.component';
import {PageFrameComponent} from '../../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-new', selector: 'app-new',
templateUrl: './new.component.html', templateUrl: './new.component.html',
styleUrls: ['./new.component.less'], styleUrls: ['./new.component.less'],
imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent], imports: [CardComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, ButtonRowComponent, ButtonComponent, PageFrameComponent],
}) })
export class NewComponent implements OnInit { export class NewComponent implements OnInit {
private songService = inject(SongService); private songService = inject(SongService);

View File

@@ -1,4 +1,5 @@
<div class="split"> <app-page-frame title="Lieder" [withMenu]="false">
<div class="split" content>
@if (song$ | async; as song) { @if (song$ | async; as song) {
<app-card [heading]="song.number + ' - ' + song.title" closeLink="../"> <app-card [heading]="song.number + ' - ' + song.title" closeLink="../">
<div class="song"> <div class="song">
@@ -10,46 +11,59 @@
<div>Status: {{ (song.status | status) || "entwurf" }}</div> <div>Status: {{ (song.status | status) || "entwurf" }}</div>
@if (song.legalOwner) { @if (song.legalOwner) {
<div>Rechteinhaber: {{ song.legalOwner | legalOwner }}</div> <div>Rechteinhaber: {{ song.legalOwner | legalOwner }}</div>
} @if (song.legalOwnerId && song.legalOwner === 'CCLI') { }
@if (song.legalOwnerId && song.legalOwner === 'CCLI') {
<div> <div>
<a href="https://songselect.ccli.com/Songs/{{ song.legalOwnerId }}" target="_blank"> CCLI Nummer: {{ song.legalOwnerId }} </a> <a href="https://songselect.ccli.com/Songs/{{ song.legalOwnerId }}" target="_blank"> CCLI
Nummer: {{ song.legalOwnerId }} </a>
</div> </div>
} @if (song.legalOwnerId && song.legalOwner !== 'CCLI') { }
@if (song.legalOwnerId && song.legalOwner !== 'CCLI') {
<div>Rechteinhaber ID: {{ song.legalOwnerId }}</div> <div>Rechteinhaber ID: {{ song.legalOwnerId }}</div>
} @if (song.artist) { }
@if (song.artist) {
<div>Künstler: {{ song.artist }}</div> <div>Künstler: {{ song.artist }}</div>
} @if (song.label) { }
@if (song.label) {
<div>Verlag: {{ song.label }}</div> <div>Verlag: {{ song.label }}</div>
} @if (song.origin) { }
@if (song.origin) {
<div>Quelle: {{ song.origin }}</div> <div>Quelle: {{ song.origin }}</div>
} }
<div [matTooltip]="songUsageTooltip$ | async" matTooltipPosition="above">Wie oft verwendet: {{ songCount$ | async }}</div> <div [matTooltip]="songUsageTooltip$ | async" matTooltipPosition="above">Wie oft
verwendet: {{ songCount$ | async }}
</div>
</div> </div>
</div> </div>
@if (user$ | async; as user) { @if (user$ | async; as user) {
<app-song-text [chordMode]="user.chordMode" [showSwitch]="true" [text]="song.text" [validateChordNotation]="true"></app-song-text> <app-song-text [chordMode]="user.chordMode" [showSwitch]="true" [text]="song.text"
[validateChordNotation]="true"></app-song-text>
} }
<mat-chip-listbox *appRole="['leader', 'contributor']" aria-label="Attribute"> <mat-chip-listbox *appRole="['leader', 'contributor']" aria-label="Attribute">
@for (flag of getFlags(song.flags); track flag) { @for (flag of getFlags(song.flags); track flag) {
<mat-chip-option>{{ flag }} </mat-chip-option> <mat-chip-option>{{ flag }}</mat-chip-option>
} }
</mat-chip-listbox> </mat-chip-listbox>
<div *appRole="['leader', 'contributor']" class="text">{{ song.comment }}</div> <div *appRole="['leader', 'contributor']" class="text">{{ song.comment }}</div>
</div> </div>
<app-button-row> <app-button-row>
<app-button (click)="onDelete(song.id)" *appRole="['admin']" [icon]="faDelete">Löschen </app-button> <app-button (click)="onDelete(song.id)" *appRole="['admin']" [icon]="faDelete">Löschen</app-button>
<app-button *appRole="['contributor']" [icon]="faEdit" routerLink="edit">Bearbeiten </app-button> <app-button *appRole="['contributor']" [icon]="faEdit" routerLink="edit">Bearbeiten</app-button>
<ng-container *appRole="['leader']"> <ng-container *appRole="['leader']">
<app-button [icon]="faFileCirclePlus" [matMenuTriggerFor]="menu"> Zu Veranstaltung hinzufügen </app-button> <app-button [icon]="faFileCirclePlus" [matMenuTriggerFor]="menu"> Zu Veranstaltung hinzufügen</app-button>
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
@for (show of privateShows$|async; track show.id) { @for (show of privateShows$|async; track show.id) {
<app-button (click)="addSongToShow(show, song)"> {{ show.date.toDate() | date: "dd.MM.yyyy" }} {{ show.showType | showType }} </app-button> <app-button
(click)="addSongToShow(show, song)"> {{ show.date.toDate() | date: "dd.MM.yyyy" }} {{ show.showType | showType }}
</app-button>
} }
</mat-menu> </mat-menu>
</ng-container> </ng-container>
</app-button-row> </app-button-row>
</app-card> </app-card>
} @if (files$ | async; as files) { @if (files.length > 0) { }
@if (files$ | async; as files) {
@if (files.length > 0) {
<app-card heading="Anhänge"> <app-card heading="Anhänge">
@for (file of files$ | async; track file.id) { @for (file of files$ | async; track file.id) {
<p> <p>
@@ -57,5 +71,7 @@
</p> </p>
} }
</app-card> </app-card>
} } }
</div> }
</div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {ActivatedRoute, Router, RouterLink} from '@angular/router'; import {ActivatedRoute, Router, RouterLink} from '@angular/router';
import {SongService} from '../services/song.service'; import {SongService} from '../services/song.service';
import {distinctUntilChanged, map, switchMap} from 'rxjs/operators'; import {distinctUntilChanged, map, switchMap} from 'rxjs/operators';
@@ -26,6 +26,7 @@ import {LegalOwnerPipe} from '../../../widget-modules/pipes/legal-owner-translat
import {StatusPipe} from '../../../widget-modules/pipes/status-translater/status.pipe'; import {StatusPipe} from '../../../widget-modules/pipes/status-translater/status.pipe';
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 {MatTooltip} from '@angular/material/tooltip'; import {MatTooltip} from '@angular/material/tooltip';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-song', selector: 'app-song',
@@ -50,6 +51,7 @@ import {MatTooltip} from '@angular/material/tooltip';
StatusPipe, StatusPipe,
ShowTypePipe, ShowTypePipe,
MatTooltip, MatTooltip,
PageFrameComponent,
], ],
}) })
export class SongComponent implements OnInit { export class SongComponent implements OnInit {
@@ -84,14 +86,14 @@ export class SongComponent implements OnInit {
const song$ = this.activatedRoute.params.pipe( const song$ = this.activatedRoute.params.pipe(
map(param => param as {songId: string}), map(param => param as {songId: string}),
map(param => param.songId), map(param => param.songId),
switchMap(songId => this.songService.read$(songId)) switchMap(songId => this.songService.read$(songId)),
); );
this.song$ = song$; this.song$ = song$;
this.files$ = this.activatedRoute.params.pipe( this.files$ = this.activatedRoute.params.pipe(
map(param => param as {songId: string}), map(param => param as {songId: string}),
map(param => param.songId), map(param => param.songId),
switchMap(songId => this.fileService.read$(songId)) switchMap(songId => this.fileService.read$(songId)),
); );
this.songCount$ = combineLatest([this.userService.user$, song$]).pipe( this.songCount$ = combineLatest([this.userService.user$, song$]).pipe(
@@ -102,7 +104,7 @@ export class SongComponent implements OnInit {
return user?.songUsage?.[song.id] ?? 0; return user?.songUsage?.[song.id] ?? 0;
}), }),
distinctUntilChanged() distinctUntilChanged(),
); );
this.songUsageShows$ = combineLatest([this.userService.user$, this.showService.list$(), song$]).pipe( this.songUsageShows$ = combineLatest([this.userService.user$, this.showService.list$(), song$]).pipe(
@@ -115,7 +117,7 @@ export class SongComponent implements OnInit {
.filter(show => show.owner === user.id) .filter(show => show.owner === user.id)
.filter(show => (show.songIds ?? []).includes(song.id)) .filter(show => (show.songIds ?? []).includes(song.id))
.sort((a, b) => b.date.toMillis() - a.date.toMillis()); .sort((a, b) => b.date.toMillis() - a.date.toMillis());
}) }),
); );
this.songUsageTooltip$ = combineLatest([this.songCount$, this.songUsageShows$]).pipe( this.songUsageTooltip$ = combineLatest([this.songCount$, this.songUsageShows$]).pipe(
@@ -129,7 +131,7 @@ export class SongComponent implements OnInit {
} }
return shows.map(show => `${this.dateFormatter.format(show.date.toDate())} - ${this.showTypePipe.transform(show.showType)}`).join('\n'); return shows.map(show => `${this.dateFormatter.format(show.date.toDate())} - ${this.showTypePipe.transform(show.showType)}`).join('\n');
}) }),
); );
} }

View File

@@ -1,8 +1,11 @@
@if (user$ | async; as user) { <app-page-frame title="Benutzer" [withMenu]="false">
<app-card heading="Hallo {{ user.name }}"> <div content>
@if (user$ | async; as user) {
<app-card heading="Hallo {{ user.name }}">
<p> <p>
@if (getUserRoles(user.role).length === 0) { @if (getUserRoles(user.role).length === 0) {
<span class="warn">Es wurden noch keine Berechtigungen zugeteilt, bitte wende Dich an den Administrator!</span> <span
class="warn">Es wurden noch keine Berechtigungen zugeteilt, bitte wende Dich an den Administrator!</span>
} }
<span>{{ transdormUserRoles(user.role) }}</span> <span>{{ transdormUserRoles(user.role) }}</span>
</p> </p>
@@ -11,15 +14,17 @@
<mat-select (ngModelChange)="onChordModeChanged(user.id, $event)" [ngModel]="user.chordMode"> <mat-select (ngModelChange)="onChordModeChanged(user.id, $event)" [ngModel]="user.chordMode">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<mat-option value="hide">nur den Liedtext anzeigen</mat-option> <mat-option value="hide">nur den Liedtext anzeigen</mat-option>
<mat-option value="onlyFirst">in Strophen die Akkorde nur für die erste anzeigen </mat-option> <mat-option value="onlyFirst">in Strophen die Akkorde nur für die erste anzeigen</mat-option>
<mat-option value="show">alle anzeigen</mat-option> <mat-option value="show">alle anzeigen</mat-option>
</mat-select> </mat-select>
<mat-hint>Das ist nur die Voreinstellung, die Anzeige kann für jedes Lied geändert werden. </mat-hint> <mat-hint>Das ist nur die Voreinstellung, die Anzeige kann für jedes Lied geändert werden.</mat-hint>
</mat-form-field> </mat-form-field>
<app-button-row> <app-button-row>
<app-button [icon]="faSignOut" routerLink="../logout">Abmelden</app-button> <app-button [icon]="faSignOut" routerLink="../logout">Abmelden</app-button>
</app-button-row> </app-button-row>
</app-card> </app-card>
} }
<app-users *appRole="['admin']"></app-users> <app-users *appRole="['admin']"></app-users>
</div>
</app-page-frame>

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {User} from '../../../services/user/user'; import {User} from '../../../services/user/user';
@@ -17,6 +17,7 @@ import {ButtonComponent} from '../../../widget-modules/components/button/button.
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {RoleDirective} from '../../../services/user/role.directive'; import {RoleDirective} from '../../../services/user/role.directive';
import {UsersComponent} from './users/users.component'; import {UsersComponent} from './users/users.component';
import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component';
@Component({ @Component({
selector: 'app-info', selector: 'app-info',
@@ -37,6 +38,7 @@ import {UsersComponent} from './users/users.component';
RoleDirective, RoleDirective,
UsersComponent, UsersComponent,
AsyncPipe, AsyncPipe,
PageFrameComponent,
], ],
}) })
export class InfoComponent implements OnInit { export class InfoComponent implements OnInit {

View File

@@ -14,9 +14,11 @@
@if (errorMessage) { @if (errorMessage) {
<p class="error">{{ errorMessage | authMessage }}</p> <p class="error">{{ errorMessage | authMessage }}</p>
} }
<button (click)="onLogin()" class="btn-login" color="primary" mat-stroked-button>Anmelden</button> <div class="buttons">
<button class="btn-password" mat-stroked-button routerLink="/user/password">Passwort zurücksetzen</button> <app-button class="full-width" (click)="onLogin()">Anmelden</app-button>
<button class="btn-user" mat-stroked-button routerLink="/user/new">neuen Benutzer anlegen</button> <app-button class="full-width" routerLink="/user/password">Passwort zurücksetzen</app-button>
<app-button class="full-width" routerLink="/user/new">neuen Benutzer anlegen</app-button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -25,21 +25,11 @@ p.error {
} }
} }
button { .buttons {
font-size: 18px; display: flex;
} flex-direction: column;
gap: var(--gap-l);
.btn-login { margin-top: var(--gap-l);
margin-bottom: 40px;
}
.btn-password {
margin-bottom: 20px;
color: var(--text-soft);
}
.btn-user {
color: var(--text-soft);
} }
.frame { .frame {

View File

@@ -1,4 +1,4 @@
import {Component, OnInit, inject} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {Router, RouterLink} from '@angular/router'; import {Router, RouterLink} from '@angular/router';
import {UserService} from '../../../services/user/user.service'; import {UserService} from '../../../services/user/user.service';
@@ -6,15 +6,14 @@ import {faSignInAlt, faUserPlus} from '@fortawesome/free-solid-svg-icons';
import {LogoComponent} from '../../../widget-modules/components/logo/logo.component'; import {LogoComponent} from '../../../widget-modules/components/logo/logo.component';
import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input'; import {MatInput} from '@angular/material/input';
import {MatButton} from '@angular/material/button';
import {AuthMessagePipe} from './auth-message.pipe'; import {AuthMessagePipe} from './auth-message.pipe';
import {ButtonComponent} from '../../../widget-modules/components/button/button.component';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.less'], styleUrls: ['./login.component.less'],
imports: [LogoComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatButton, RouterLink, AuthMessagePipe], imports: [LogoComponent, ReactiveFormsModule, MatFormField, MatLabel, MatInput, RouterLink, AuthMessagePipe, ButtonComponent],
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
private userService = inject(UserService); private userService = inject(UserService);

View File

@@ -0,0 +1,48 @@
import {DOCUMENT} from '@angular/common';
import {Injectable, inject, signal} from '@angular/core';
type ThemeMode = 'light' | 'dark';
@Injectable({providedIn: 'root'})
export class ThemeService {
private readonly document = inject(DOCUMENT);
private readonly storageKey = 'wgenerator-theme';
public readonly theme = signal<ThemeMode>('light');
public readonly isDarkMode = signal(false);
public constructor() {
this.initializeTheme();
}
public toggleTheme(): void {
this.setTheme(this.isDarkMode() ? 'light' : 'dark');
}
public setTheme(theme: ThemeMode): void {
this.theme.set(theme);
this.isDarkMode.set(theme === 'dark');
this.applyTheme(theme);
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.storageKey, theme);
}
}
private initializeTheme(): void {
const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem(this.storageKey) : null;
if (storedTheme === 'light' || storedTheme === 'dark') {
this.setTheme(storedTheme);
return;
}
const prefersDarkMode = typeof window !== 'undefined'
&& typeof window.matchMedia === 'function'
&& window.matchMedia('(prefers-color-scheme: dark)').matches;
this.setTheme(prefersDarkMode ? 'dark' : 'light');
}
private applyTheme(theme: ThemeMode): void {
this.document.body.classList.toggle('theme-dark', theme === 'dark');
this.document.documentElement.style.colorScheme = theme;
}
}

View File

@@ -6,7 +6,15 @@
<app-link *appRole="['presenter']" [icon]="faPresentation" link="/presentation" text="Präsentation"></app-link> <app-link *appRole="['presenter']" [icon]="faPresentation" link="/presentation" text="Präsentation"></app-link>
<app-link [icon]="faUser" link="/user" text="Benutzer"></app-link> <app-link [icon]="faUser" link="/user" text="Benutzer"></app-link>
</div> </div>
<div *appRole="['user', 'presenter', 'leader']" class="actions"> <div *appRole="['user', 'presenter', 'leader']" class="actions actions-search">
<button
mat-icon-button
class="theme-toggle"
[attr.aria-label]="themeService.isDarkMode() ? 'Lightmode aktivieren' : 'Darkmode aktivieren'"
[attr.title]="themeService.isDarkMode() ? 'Zum Lightmode wechseln' : 'Zum Darkmode wechseln'"
(click)="themeService.toggleTheme()">
<fa-icon [icon]="themeService.isDarkMode() ? faLightMode : faDarkMode"></fa-icon>
</button>
<app-filter></app-filter> <app-filter></app-filter>
</div> </div>
</nav> </nav>

View File

@@ -10,7 +10,7 @@ nav {
height: 50px; height: 50px;
background: var(--navigation-background); background: var(--navigation-background);
color: var(--text-inverse); color: var(--text-inverse);
z-index: 1; z-index: 20;
box-shadow: 0px -5px 20px 4px rgba(0, 0, 0, 0.39), 1px 0px 6px 4px rgba(0, 0, 0, 0.53); box-shadow: 0px -5px 20px 4px rgba(0, 0, 0, 0.39), 1px 0px 6px 4px rgba(0, 0, 0, 0.53);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
transition: var(--transition); transition: var(--transition);
@@ -21,7 +21,7 @@ nav {
&.hidden { &.hidden {
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
top: -60px; // top: -60px;
} }
} }
@@ -32,9 +32,26 @@ nav {
display: flex; display: flex;
height: 100%; height: 100%;
align-items: center; align-items: center;
padding-right: 12px;
}
.actions-search {
gap: var(--gap-s);
padding-right: 20px; padding-right: 20px;
} }
.theme-toggle {
color: var(--text-inverse);
transition: var(--transition-fast);
&:hover {
background: var(--hover-background);
}
}
.theme-toggle fa-icon {
font-size: 14px;
}
.links { .links {
display: flex; display: flex;

View File

@@ -1,5 +1,7 @@
import {Component} from '@angular/core'; import {Component, inject} from '@angular/core';
import {faChalkboard, faMusic, faPersonBooth, faUserCog} from '@fortawesome/free-solid-svg-icons'; import {MatIconButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faChalkboard, faMoon, faMusic, faPersonBooth, faSun, faUserCog} from '@fortawesome/free-solid-svg-icons';
import {fromEvent, Observable} from 'rxjs'; import {fromEvent, Observable} from 'rxjs';
import {distinctUntilChanged, map, shareReplay, startWith} from 'rxjs/operators'; import {distinctUntilChanged, map, shareReplay, startWith} from 'rxjs/operators';
import {BrandComponent} from './brand/brand.component'; import {BrandComponent} from './brand/brand.component';
@@ -8,18 +10,22 @@ import {RoleDirective} from '../../../../services/user/role.directive';
import {LinkComponent} from './link/link.component'; import {LinkComponent} from './link/link.component';
import {FilterComponent} from './filter/filter.component'; import {FilterComponent} from './filter/filter.component';
import {AsyncPipe} from '@angular/common'; import {AsyncPipe} from '@angular/common';
import {ThemeService} from '../../../../services/theme/theme.service';
@Component({ @Component({
selector: 'app-navigation', selector: 'app-navigation',
templateUrl: './navigation.component.html', templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.less'], styleUrls: ['./navigation.component.less'],
imports: [BrandComponent, RouterLink, RoleDirective, LinkComponent, FilterComponent, AsyncPipe], imports: [BrandComponent, RouterLink, RoleDirective, LinkComponent, FilterComponent, AsyncPipe, MatIconButton, FaIconComponent],
}) })
export class NavigationComponent { export class NavigationComponent {
public readonly themeService = inject(ThemeService);
public faSongs = faMusic; public faSongs = faMusic;
public faShows = faPersonBooth; public faShows = faPersonBooth;
public faUser = faUserCog; public faUser = faUserCog;
public faPresentation = faChalkboard; public faPresentation = faChalkboard;
public faDarkMode = faMoon;
public faLightMode = faSun;
public readonly windowScroll$: Observable<number> = fromEvent(window, 'scroll').pipe( public readonly windowScroll$: Observable<number> = fromEvent(window, 'scroll').pipe(
map(() => window.scrollY), map(() => window.scrollY),

View File

@@ -1,6 +1,11 @@
.row { .row {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
@media screen and (max-width: 860px) {
flex-direction: column-reverse;
}
gap: var(--gap-m);
width: 100%; width: 100%;
flex-wrap: wrap; flex-wrap: wrap;
padding-top: var(--gap-l);
} }

View File

@@ -1,4 +1,4 @@
<button [disabled]="disabled" mat-button> <button [disabled]="disabled">
@if (icon) { @if (icon) {
<span><fa-icon [icon]="icon"></fa-icon><span class="content">&nbsp;</span></span> <span><fa-icon [icon]="icon"></fa-icon><span class="content">&nbsp;</span></span>
} }

View File

@@ -1,15 +1,19 @@
:host {
display: inline-flex;
}
:host(.full-width) { :host(.full-width) {
display: flex;
width: 100%; width: 100%;
} }
button { button {
color: var(--text); display: flex;
color: var(--primary-color);
transition: var(--transition); transition: var(--transition);
border-radius: 4px;
font-size: 1rem;
background: var(--surface-strong);
border: 1px solid var(--primary-color);
padding: var(--gap-s);
cursor: pointer;
:host(.full-width) & { :host(.full-width) & {
width: 100%; width: 100%;
@@ -18,18 +22,19 @@ button {
&:hover { &:hover {
color: var(--primary-active); color: var(--primary-active);
background: var(--surface-subtle);
} }
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
font-size: 30px; font-size: 1.2rem;
width: 100%;
justify-content: center;
.button-content {
flex-grow: 1;
}
} }
} }
.button-content {
@media screen and (max-width: 860px) {
display: none ;
}
}
fa-icon { fa-icon {
width: 20px; width: 20px;

View File

@@ -1,6 +1,5 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {IconProp} from '@fortawesome/fontawesome-svg-core'; import {IconProp} from '@fortawesome/fontawesome-svg-core';
import {MatButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FaIconComponent} from '@fortawesome/angular-fontawesome';
@@ -8,7 +7,7 @@ import {FaIconComponent} from '@fortawesome/angular-fontawesome';
selector: 'app-button', selector: 'app-button',
templateUrl: './button.component.html', templateUrl: './button.component.html',
styleUrls: ['./button.component.less'], styleUrls: ['./button.component.less'],
imports: [MatButton, FaIconComponent], imports: [FaIconComponent],
host: { host: {
'[class.full-width]': 'fullWidth', '[class.full-width]': 'fullWidth',
}, },

View File

@@ -3,9 +3,11 @@
<button [routerLink]="closeLink" class="btn-close" mat-icon-button> <button [routerLink]="closeLink" class="btn-close" mat-icon-button>
<fa-icon [icon]="closeIcon"></fa-icon> <fa-icon [icon]="closeIcon"></fa-icon>
</button> </button>
} @if (heading && !fullscreen) { }
@if (heading && !fullscreen) {
<div class="heading">{{ heading }}</div> <div class="heading">{{ heading }}</div>
} @if (subheading && !fullscreen) { }
@if (subheading && !fullscreen) {
<div class="subheading">{{ subheading }}</div> <div class="subheading">{{ subheading }}</div>
} }
<ng-content></ng-content> <ng-content></ng-content>

View File

@@ -1,7 +1,7 @@
@import "../../../../styles/shadow"; @import "../../../../styles/shadow";
.card { .card {
margin: 20px; margin: var(--gap-l) var(--gap-l) 0;
border-radius: 8px; border-radius: 8px;
background: var(--surface); background: var(--surface);
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
@@ -10,19 +10,17 @@
width: 800px; width: 800px;
position: relative; position: relative;
color: var(--text); color: var(--text);
padding: 10px 0; padding: var(--gap-m) 0;
box-shadow: var(--shadow-card-3); box-shadow: var(--shadow-card-3);
@media screen and (max-width: 860px) { @media screen and (max-width: 860px) {
width: 100vw; width: calc(100vw - 10px);
border-radius: 0; margin: 5px;
background: var(--surface-strong);
margin: 0;
color: var(--text); color: var(--text);
} }
&.padding { &.padding {
padding: 20px; padding: var(--gap-l);
} }
box-sizing: border-box; box-sizing: border-box;
@@ -38,7 +36,7 @@
top: 0; top: 0;
bottom: 0; bottom: 0;
width: unset; width: unset;
z-index: 10; z-index: 30;
} }
} }

View File

@@ -0,0 +1,27 @@
@if (withMenu() || title()) {
<div class="header">
@if (withMenu()) {
<button (click)="toggle()"
[attr.aria-expanded]="!collapsed"
aria-label="Sidebar umschalten"
class="sidebar-toggle"
mat-icon-button type="button">
<fa-icon [icon]="collapsed ? closedIcon : openIcon"></fa-icon>
</button>
}
<div class="title">{{ title() }}</div>
</div>
}
@if (!collapsed) {
<button (click)="close()" aria-label="Sidebar schließen" class="sidebar-backdrop" tabindex="-1"
type="button"></button>
}
<aside [class.collapsed]="collapsed">
<div aria-hidden="true" class="sidebar-toggle-placeholder"></div>
<div class="sidebar-body">
<ng-content select="[sidebar]"></ng-content>
</div>
</aside>
<div class="content">
<ng-content select="[content]"></ng-content>
</div>

View File

@@ -19,20 +19,20 @@
.sidebar-toggle { .sidebar-toggle {
--icon-button-color: var(--primary-hover); --icon-button-color: var(--primary-hover);
--icon-button-hover-color: var(--primary-active); --icon-button-hover-color: var(--primary);
position: fixed; position: fixed;
top: calc(50px + var(--sidebar-toggle-offset)); top: calc(50px + var(--sidebar-toggle-offset));
left: var(--sidebar-toggle-offset); left: var(--sidebar-toggle-offset);
z-index: 11; z-index: 11;
color: var(--icon-button-color); color: var(--icon-button-color);
background: var(--surface-strong);
box-shadow: none; box-shadow: none;
border-radius: 999px; border-radius: 999px;
transition: all 150ms ease-in-out;
} }
:host.collapsed .sidebar-toggle { :host.collapsed .sidebar-toggle {
--icon-button-color: var(--text); --icon-button-color: var(--surface-persist);
--icon-button-hover-color: var(--primary-active); --icon-button-hover-color: var(--surface-subtle);
} }
.sidebar-toggle fa-icon { .sidebar-toggle fa-icon {
@@ -92,7 +92,7 @@ aside.collapsed {
.sidebar-toggle { .sidebar-toggle {
top: calc(50px + 8px); top: calc(50px + 8px);
left: 12px; left: 5px;
right: auto; right: auto;
z-index: 13; z-index: 13;
} }
@@ -121,6 +121,32 @@ aside.collapsed {
.content { .content {
grid-column: auto; grid-column: auto;
width: 100%; width: 100%;
padding-top: calc(var(--sidebar-toggle-size) + 6px); }
}
.header {
@media screen and (max-width: 860px) {
height: 50px;
}
}
.title {
color: var(--surface-persist);
font-size: 30px;
text-transform: uppercase;
text-align: right;
opacity: 0.5;
text-shadow: var(--shadow-card-3);
position: fixed;
right: 10px;
top: 61px;
transition: all 150ms ease-in-out;
@media screen and (min-width: 888px) {
transform-origin: top right;
transform: rotate(90deg) translateX(100%) translateX(10px);
text-shadow: 10px 0 20px rgba(0, 0, 0, 0.19), 6px 0 6px rgba(0, 0, 0, 0.23);
} }
} }

View File

@@ -1,17 +1,17 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SidebarComponent} from './sidebar.component'; import {PageFrameComponent} from './page-frame.component';
describe('SidebarComponent', () => { describe('SidebarComponent', () => {
let component: SidebarComponent; let component: PageFrameComponent;
let fixture: ComponentFixture<SidebarComponent>; let fixture: ComponentFixture<PageFrameComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SidebarComponent], imports: [PageFrameComponent],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(SidebarComponent); fixture = TestBed.createComponent(PageFrameComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
await fixture.whenStable(); await fixture.whenStable();
}); });

View File

@@ -1,21 +1,23 @@
import {Component} from '@angular/core'; import {Component, input} from '@angular/core';
import {MatIconButton} from '@angular/material/button'; import {MatIconButton} from '@angular/material/button';
import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {faBars, faChevronLeft} from '@fortawesome/free-solid-svg-icons'; import {faBars, faChevronLeft} from '@fortawesome/free-solid-svg-icons';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-page-frame',
imports: [MatIconButton, FaIconComponent], imports: [MatIconButton, FaIconComponent],
templateUrl: './sidebar.component.html', templateUrl: './page-frame.component.html',
styleUrl: './sidebar.component.less', styleUrl: './page-frame.component.less',
host: { host: {
'[class.collapsed]': 'collapsed', '[class.collapsed]': 'collapsed',
}, },
}) })
export class SidebarComponent { export class PageFrameComponent {
public collapsed = true; public collapsed = true;
public openIcon = faChevronLeft; public openIcon = faChevronLeft;
public closedIcon = faBars; public closedIcon = faBars;
public title = input.required<string>();
public withMenu = input<boolean>(true);
public toggle(): void { public toggle(): void {
this.collapsed = !this.collapsed; this.collapsed = !this.collapsed;

View File

@@ -1,15 +0,0 @@
<button (click)="toggle()" [attr.aria-expanded]="!collapsed" aria-label="Sidebar umschalten" class="sidebar-toggle" mat-icon-button type="button">
<fa-icon [icon]="collapsed ? closedIcon : openIcon"></fa-icon>
</button>
@if (!collapsed) {
<button (click)="close()" aria-label="Sidebar schließen" class="sidebar-backdrop" tabindex="-1" type="button"></button>
}
<aside [class.collapsed]="collapsed">
<div aria-hidden="true" class="sidebar-toggle-placeholder"></div>
<div class="sidebar-body">
<ng-content select="[sidebar]"></ng-content>
</div>
</aside>
<div class="content">
<ng-content select="[content]"></ng-content>
</div>

View File

@@ -5,18 +5,123 @@
@include mat.elevation-classes(); @include mat.elevation-classes();
@include mat.app-background(); @include mat.app-background();
$wgenerator-primary: mat.m2-define-palette(mat.$m2-indigo-palette); $wgenerator-material-theme: mat.define-theme((
$wgenerator-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
$wgenerator-warn: mat.m2-define-palette(mat.$m2-red-palette);
$wgenerator-theme: mat.m2-define-light-theme((
color: ( color: (
primary: $wgenerator-primary, theme-type: light,
accent: $wgenerator-accent, primary: mat.$azure-palette,
warn: $wgenerator-warn, tertiary: mat.$blue-palette,
use-system-variables: true,
),
typography: (
use-system-variables: true,
),
density: (
scale: -2,
), ),
typography: mat.m2-define-typography-config(),
density: -2,
)); ));
@include mat.all-component-themes($wgenerator-theme); @include mat.all-component-themes($wgenerator-material-theme);
:root {
@include mat.theme((
color: (
theme-type: light,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: -2,
));
@include mat.theme-overrides((
background: var(--surface),
error: var(--danger),
error-container: color-mix(in srgb, var(--danger) 18%, var(--surface-strong)),
inverse-on-surface: var(--text-inverse),
inverse-primary: var(--primary-hover),
inverse-surface: var(--bg-deep),
on-background: var(--text),
on-error: var(--text-inverse),
on-error-container: var(--text),
on-primary: var(--text-inverse),
on-primary-container: var(--text),
on-secondary: var(--text-inverse),
on-secondary-container: var(--text),
on-surface: var(--text),
on-surface-variant: var(--text-soft),
on-tertiary: var(--text-inverse),
on-tertiary-container: var(--text),
outline: var(--surface-border),
outline-variant: var(--divider),
primary: var(--primary-color),
primary-container: color-mix(in srgb, var(--primary-color) 22%, var(--surface-strong)),
secondary: var(--primary-hover),
secondary-container: color-mix(in srgb, var(--primary-hover) 20%, var(--surface-strong)),
scrim: rgba(18, 24, 37, 0.62),
shadow: rgba(18, 24, 37, 0.28),
surface: var(--surface),
surface-bright: var(--surface-strong),
surface-container: color-mix(in srgb, var(--surface) 88%, white),
surface-container-high: color-mix(in srgb, var(--surface-strong) 92%, var(--primary-hover)),
surface-container-highest: color-mix(in srgb, var(--surface-strong) 84%, var(--primary-hover)),
surface-container-low: color-mix(in srgb, var(--surface) 96%, white),
surface-container-lowest: var(--surface-strong),
surface-dim: color-mix(in srgb, var(--surface) 78%, var(--surface-dark)),
surface-tint: var(--primary-color),
surface-variant: color-mix(in srgb, var(--surface) 72%, var(--primary-hover)),
tertiary: var(--accent-color),
tertiary-container: color-mix(in srgb, var(--accent-color) 18%, var(--surface-strong)),
));
}
body.theme-dark {
@include mat.theme((
color: (
theme-type: dark,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: -2,
));
@include mat.theme-overrides((
background: var(--surface-dark),
error: var(--danger),
error-container: color-mix(in srgb, var(--danger) 24%, rgba(255, 255, 255, 0.18)),
inverse-on-surface: var(--text),
inverse-primary: var(--primary-color),
inverse-surface: var(--text-inverse),
on-background: var(--text),
on-error: var(--text-inverse),
on-error-container: var(--text),
on-primary: #07111b,
on-primary-container: var(--text),
on-secondary: #07111b,
on-secondary-container: var(--text),
on-surface: var(--text),
on-surface-variant: color-mix(in srgb, var(--text) 78%, white),
on-tertiary: #07111b,
on-tertiary-container: var(--text),
outline: rgba(206, 223, 229, 0.34),
outline-variant: rgba(206, 223, 229, 0.22),
primary: var(--primary-hover),
primary-container: color-mix(in srgb, var(--primary-color) 36%, rgba(255, 255, 255, 0.18)),
secondary: color-mix(in srgb, var(--primary-hover) 92%, white),
secondary-container: color-mix(in srgb, var(--primary-active) 32%, rgba(255, 255, 255, 0.18)),
scrim: rgba(4, 10, 18, 0.82),
shadow: rgba(0, 0, 0, 0.45),
surface: rgba(36, 54, 72, 0.96),
surface-bright: rgba(46, 68, 89, 0.98),
surface-container: rgba(41, 61, 80, 0.97),
surface-container-high: rgba(48, 71, 93, 0.98),
surface-container-highest: rgba(57, 82, 106, 0.98),
surface-container-low: rgba(32, 48, 63, 0.94),
surface-container-lowest: rgba(25, 38, 50, 0.94),
surface-dim: rgba(20, 31, 42, 0.92),
surface-tint: var(--primary-hover),
surface-variant: rgba(52, 75, 96, 0.95),
tertiary: var(--accent-color),
tertiary-container: color-mix(in srgb, var(--accent-color) 26%, rgba(255, 255, 255, 0.18)),
));
}

View File

@@ -4,6 +4,7 @@
--bg-soft: #bbd2c5; --bg-soft: #bbd2c5;
--surface: rgba(244, 247, 245, 0.88); --surface: rgba(244, 247, 245, 0.88);
--surface-persist: rgba(244, 247, 245, 0.88);
--surface-strong: rgba(255, 255, 255, 0.96); --surface-strong: rgba(255, 255, 255, 0.96);
--surface-dark: rgba(30, 36, 52, 0.72); --surface-dark: rgba(30, 36, 52, 0.72);
--surface-border: rgba(41, 46, 73, 0.16); --surface-border: rgba(41, 46, 73, 0.16);
@@ -14,8 +15,10 @@
--text-soft: #7a858c; --text-soft: #7a858c;
--text-inverse: #f7fbff; --text-inverse: #f7fbff;
--color-primary-dark: #48686e;
--color-primary: #6f8f95; --color-primary: #6f8f95;
--color-primary-light: #85a4aa; --color-primary-light: #85a4aa;
--primary-color-sat: #578f9a;
--primary-color: #6f8f95; --primary-color: #6f8f95;
--primary-hover: #85a4aa; --primary-hover: #85a4aa;
--primary-active: #5b797e; --primary-active: #5b797e;
@@ -41,6 +44,173 @@
--mat-dialog-supporting-text-color: var(--text); --mat-dialog-supporting-text-color: var(--text);
--mat-button-text-label-text-color: var(--color-primary-dark);
--gap-l: 20px;
--gap-m: calc(var(--gap-l) / 1.618);
--gap-s: calc(var(--gap-m) / 1.618);
}
body.theme-dark {
--bg-deep: #07111b;
--bg-mid: #102435;
--bg-soft: #1d3d4a;
--surface: rgba(13, 22, 34, 0.82);
--surface-strong: rgba(18, 31, 46, 0.94);
--surface-dark: rgba(4, 10, 18, 0.88);
--surface-border: rgba(167, 198, 206, 0.24);
--surface-subtle: rgba(255, 255, 255, 0.06);
--surface-muted: rgba(167, 198, 206, 0.08);
--text: #e8f0f4;
--text-soft: #b7c9cf;
--text-inverse: #f7fbff;
--color-primary-dark: #89acb5;
--color-primary: #7ea2ab;
--color-primary-light: #a7c6ce;
--primary-color-sat: #73bbc7;
--primary-color: #7ea2ab;
--primary-hover: #a7c6ce;
--primary-active: #638892;
--accent-color: #8ce3ca;
--navigation-background: rgba(4, 10, 18, 0.82);
--hover-background: rgba(126, 162, 171, 0.18);
--overlay: rgba(4, 10, 18, 0.68);
--overlay-strong: rgba(4, 10, 18, 0.9);
--divider: rgba(232, 240, 244, 0.12);
--link-color: var(--primary-hover);
--focus-ring: 0 0 0 2px rgba(126, 162, 171, 0.34);
--icon-button-color: var(--primary-hover);
--icon-button-hover-color: var(--text-inverse);
--mat-button-text-label-text-color: var(--text);
--mat-option-label-text-color: var(--text);
--mat-option-selected-state-label-text-color: var(--primary-hover);
--mat-optgroup-label-text-color: var(--text-soft);
--mat-select-enabled-trigger-text-color: var(--text);
--mat-select-disabled-trigger-text-color: rgba(232, 240, 244, 0.38);
--mat-select-placeholder-text-color: var(--text-soft);
--mat-select-enabled-arrow-color: var(--text-soft);
--mat-select-focused-arrow-color: var(--primary-hover);
--mat-form-field-enabled-select-arrow-color: var(--text-soft);
--mat-form-field-focus-select-arrow-color: var(--primary-hover);
--mat-form-field-outlined-input-text-color: var(--text);
--mat-form-field-outlined-label-text-color: var(--text-soft);
--mat-form-field-outlined-hover-label-text-color: var(--text);
--mat-form-field-outlined-focus-label-text-color: var(--primary-hover);
--mat-form-field-outlined-outline-color: rgba(206, 223, 229, 0.22);
--mat-form-field-outlined-hover-outline-color: rgba(206, 223, 229, 0.42);
--mat-form-field-outlined-focus-outline-color: var(--primary-hover);
--mat-form-field-outlined-caret-color: var(--primary-hover);
--mat-form-field-filled-input-text-color: var(--text);
--mat-form-field-filled-label-text-color: var(--text-soft);
--mat-form-field-filled-hover-label-text-color: var(--text);
--mat-form-field-filled-focus-label-text-color: var(--primary-hover);
--mat-form-field-filled-container-color: rgba(36, 54, 72, 0.96);
--mat-form-field-filled-active-indicator-color: rgba(206, 223, 229, 0.28);
--mat-form-field-filled-hover-active-indicator-color: rgba(206, 223, 229, 0.48);
--mat-form-field-filled-focus-active-indicator-color: var(--primary-hover);
--mat-form-field-input-text-placeholder-color: var(--text-soft);
--mat-form-field-filled-input-text-placeholder-color: var(--text-soft);
--mat-form-field-outlined-input-text-placeholder-color: var(--text-soft);
--mat-dialog-container-color: rgba(41, 61, 80, 0.98);
--mat-dialog-subhead-color: var(--text);
--mat-dialog-supporting-text-color: var(--text);
--mat-select-panel-background-color: rgba(41, 61, 80, 0.98);
--mat-autocomplete-background-color: rgba(41, 61, 80, 0.98);
--mat-menu-container-color: rgba(41, 61, 80, 0.98);
--mat-menu-item-label-text-color: var(--text);
--mat-menu-item-icon-color: var(--text-soft);
--mat-menu-item-hover-state-layer-color: rgba(255, 255, 255, 0.08);
--mat-menu-item-focus-state-layer-color: rgba(255, 255, 255, 0.12);
--mat-menu-divider-color: rgba(206, 223, 229, 0.16);
--mat-checkbox-label-text-color: var(--text);
--mat-checkbox-disabled-label-color: rgba(232, 240, 244, 0.42);
--mat-checkbox-unselected-icon-color: rgba(206, 223, 229, 0.72);
--mat-checkbox-unselected-hover-icon-color: var(--text);
--mat-checkbox-unselected-focus-icon-color: var(--text);
--mat-checkbox-selected-icon-color: var(--primary-hover);
--mat-checkbox-selected-hover-icon-color: var(--primary-hover);
--mat-checkbox-selected-focus-icon-color: var(--primary-hover);
--mat-checkbox-selected-checkmark-color: #07111b;
--mat-checkbox-disabled-selected-checkmark-color: rgba(7, 17, 27, 0.72);
--mat-checkbox-disabled-selected-icon-color: rgba(206, 223, 229, 0.34);
--mat-checkbox-disabled-unselected-icon-color: rgba(206, 223, 229, 0.28);
--mat-checkbox-unselected-hover-state-layer-color: rgba(232, 240, 244, 0.12);
--mat-checkbox-unselected-focus-state-layer-color: rgba(232, 240, 244, 0.16);
--mat-checkbox-unselected-pressed-state-layer-color: rgba(167, 198, 206, 0.18);
--mat-checkbox-selected-hover-state-layer-color: rgba(167, 198, 206, 0.16);
--mat-checkbox-selected-focus-state-layer-color: rgba(167, 198, 206, 0.2);
--mat-checkbox-selected-pressed-state-layer-color: rgba(167, 198, 206, 0.22);
}
body.theme-dark .mat-mdc-text-field-wrapper {
background-color: rgba(36, 54, 72, 0.96);
}
body.theme-dark .mdc-notched-outline__leading,
body.theme-dark .mdc-notched-outline__notch,
body.theme-dark .mdc-notched-outline__trailing {
border-color: rgba(206, 223, 229, 0.22) !important;
}
body.theme-dark .mat-mdc-form-field:hover .mdc-notched-outline__leading,
body.theme-dark .mat-mdc-form-field:hover .mdc-notched-outline__notch,
body.theme-dark .mat-mdc-form-field:hover .mdc-notched-outline__trailing {
border-color: rgba(206, 223, 229, 0.42) !important;
}
body.theme-dark .mat-mdc-form-field.mat-focused .mdc-notched-outline__leading,
body.theme-dark .mat-mdc-form-field.mat-focused .mdc-notched-outline__notch,
body.theme-dark .mat-mdc-form-field.mat-focused .mdc-notched-outline__trailing {
border-color: var(--primary-hover) !important;
}
body.theme-dark .mat-mdc-input-element,
body.theme-dark .mat-mdc-select-value,
body.theme-dark .mat-mdc-select-min-line {
color: var(--text) !important;
}
body.theme-dark .mat-mdc-form-field .mat-mdc-floating-label,
body.theme-dark .mat-mdc-form-field .mat-mdc-select-placeholder {
color: var(--text-soft) !important;
}
body.theme-dark .mat-mdc-select-arrow svg {
fill: var(--text-soft) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-select-panel,
body.theme-dark .cdk-overlay-container .mat-mdc-autocomplete-panel,
body.theme-dark .cdk-overlay-container .mat-mdc-menu-panel {
background: rgba(41, 61, 80, 0.98) !important;
color: var(--text) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-select-panel,
body.theme-dark .cdk-overlay-container .mat-mdc-autocomplete-panel {
--mat-select-panel-background-color: rgba(41, 61, 80, 0.98);
--mat-autocomplete-background-color: rgba(41, 61, 80, 0.98);
}
body.theme-dark .cdk-overlay-container .mat-mdc-option .mdc-list-item__primary-text,
body.theme-dark .cdk-overlay-container .mat-mdc-option .mat-pseudo-checkbox,
body.theme-dark .cdk-overlay-container .mat-mdc-menu-item .mat-mdc-menu-item-text {
color: var(--text) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-option.mdc-list-item--selected .mdc-list-item__primary-text {
color: var(--primary-hover) !important;
}
body.theme-dark .cdk-overlay-container .mat-mdc-option:hover:not(.mdc-list-item--disabled),
body.theme-dark .cdk-overlay-container .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled),
body.theme-dark .cdk-overlay-container .mat-mdc-menu-item:hover:not([disabled]) {
background: rgba(255, 255, 255, 0.08) !important;
} }
html { html {