Cleanup Code

This commit is contained in:
2019-06-02 18:52:15 +02:00
parent 5180b597d2
commit f88454caf7
45 changed files with 1071 additions and 1060 deletions

View File

@@ -1,21 +1,22 @@
import { SongsComponent } from './components/songs/songs.component'; import {SongsComponent} from './components/songs/songs.component';
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'songs', path: 'songs',
component: SongsComponent component: SongsComponent
}, },
{ {
path: '', path: '',
redirectTo: 'songs', redirectTo: 'songs',
pathMatch: 'full' pathMatch: 'full'
} }
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule { } export class AppRoutingModule {
}

View File

@@ -1,2 +1 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -1,8 +1,9 @@
import { Component } from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.less'] styleUrls: ['./app.component.less']
}) })
export class AppComponent { } export class AppComponent {
}

View File

@@ -1,69 +1,70 @@
import { BrowserModule } from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { ODataModule } from 'odata-v4-ng'; import {ODataModule} from 'odata-v4-ng';
import { AppRoutingModule } from './app-routing.module'; import {AppRoutingModule} from './app-routing.module';
import { AppComponent } from './app.component'; import {AppComponent} from './app.component';
import { HttpClientModule } from '@angular/common/http'; import {HttpClientModule} from '@angular/common/http';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import { MatInputModule } from '@angular/material/input'; import {MatInputModule} from '@angular/material/input';
import { MatTableModule } from '@angular/material/table'; import {MatTableModule} from '@angular/material/table';
import { MatCardModule } from '@angular/material/card'; import {MatCardModule} from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips'; import {MatChipsModule} from '@angular/material/chips';
import { MatRadioModule } from '@angular/material/radio'; import {MatRadioModule} from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import {MatSelectModule} from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip'; import {MatTooltipModule} from '@angular/material/tooltip';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import {MatProgressBarModule} from '@angular/material/progress-bar';
import { TableComponent } from './components/songs/table/table.component'; import {TableComponent} from './components/songs/table/table.component';
import { SongsComponent } from './components/songs/songs.component'; import {SongsComponent} from './components/songs/songs.component';
import { SongComponent } from './components/songs/song/song.component'; import {SongComponent} from './components/songs/song/song.component';
import { SongEditComponent } from './components/songs/song-edit/song-edit.component'; import {SongEditComponent} from './components/songs/song-edit/song-edit.component';
import { SongNewComponent } from './components/songs/song-new/song-new.component'; import {SongNewComponent} from './components/songs/song-new/song-new.component';
import { SongFormComponent } from './components/songs/song-form/song-form.component'; import {SongFormComponent} from './components/songs/song-form/song-form.component';
import { SongFilesComponent } from './components/songs/song-files/song-files.component'; import {SongFilesComponent} from './components/songs/song-files/song-files.component';
import { FileUploadModule } from 'ng2-file-upload'; import {FileUploadModule} from 'ng2-file-upload';
import { SongFileEditComponent } from './components/songs/song-file-edit/song-file-edit.component'; import {SongFileEditComponent} from './components/songs/song-file-edit/song-file-edit.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
SongsComponent, SongsComponent,
TableComponent, TableComponent,
SongComponent, SongComponent,
SongEditComponent, SongEditComponent,
SongNewComponent, SongNewComponent,
SongFormComponent, SongFormComponent,
SongFilesComponent, SongFilesComponent,
SongFileEditComponent SongFileEditComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
HttpClientModule, HttpClientModule,
ODataModule, ODataModule,
MatInputModule, MatInputModule,
MatCardModule, MatCardModule,
MatTableModule, MatTableModule,
MatButtonModule, MatButtonModule,
MatChipsModule, MatChipsModule,
MatRadioModule, MatRadioModule,
MatSelectModule, MatSelectModule,
MatTooltipModule, MatTooltipModule,
MatProgressBarModule, MatProgressBarModule,
FontAwesomeModule, FontAwesomeModule,
FileUploadModule, FileUploadModule,
AppRoutingModule, AppRoutingModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule {} export class AppModule {
}

View File

@@ -1,24 +1,24 @@
<div class="song-detail-container"> <div class="song-detail-container">
<mat-card class="mat-elevation-z8" *ngIf="form"> <mat-card *ngIf="form" class="mat-elevation-z8">
<mat-card-header> <mat-card-header>
<div mat-card-avatar> <div mat-card-avatar>
<button mat-icon-button (click)="onBack()" color="warn"> <button (click)="onBack()" color="warn" mat-icon-button>
<fa-icon [icon]="faArrow"></fa-icon> <fa-icon [icon]="faArrow"></fa-icon>
</button> </button>
</div> </div>
<mat-card-title>Titel bearbeiten</mat-card-title> <mat-card-title>Titel bearbeiten</mat-card-title>
<mat-card-subtitle> <mat-card-subtitle>
Daten werden nach der Eingabe automatisch gespeichert Daten werden nach der Eingabe automatisch gespeichert
</mat-card-subtitle> </mat-card-subtitle>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<app-song-form [form]="form"></app-song-form> <app-song-form [form]="form"></app-song-form>
</mat-card-content> </mat-card-content>
<!-- <mat-card-actions> <!-- <mat-card-actions>
<button mat-button (click)="onClickDownload()">Herunterladen</button> <button mat-button (click)="onClickDownload()">Herunterladen</button>
<button mat-button (click)="onClickEdit()"> <button mat-button (click)="onClickEdit()">
<fa-icon [icon]="faEdit"></fa-icon> Bearbeiten <fa-icon [icon]="faEdit"></fa-icon> Bearbeiten
</button> </button>
</mat-card-actions> --> </mat-card-actions> -->
</mat-card> </mat-card>
</div> </div>

View File

@@ -1,3 +1,3 @@
:host { :host {
display: block; display: block;
} }

View File

@@ -1,38 +1,34 @@
import { SongsService } from 'src/app/data/songs.service'; import {SongsService} from 'src/app/data/songs.service';
import { FormGroup } from '@angular/forms'; import {FormGroup} from '@angular/forms';
import { import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core';
Component, import {EditSongService} from 'src/app/data/edit-song.service';
OnInit, import {faLongArrowAltLeft} from '@fortawesome/free-solid-svg-icons';
ChangeDetectionStrategy, import {State} from 'src/app/data/state';
ChangeDetectorRef
} from '@angular/core';
import { EditSongService } from 'src/app/data/edit-song.service';
import { faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons';
import { State } from 'src/app/data/state';
@Component({ @Component({
selector: 'app-song-edit', selector: 'app-song-edit',
templateUrl: './song-edit.component.html', templateUrl: './song-edit.component.html',
styleUrls: ['./song-edit.component.less'], styleUrls: ['./song-edit.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SongEditComponent implements OnInit { export class SongEditComponent implements OnInit {
public form: FormGroup = null; public form: FormGroup = null;
public faArrow = faLongArrowAltLeft; public faArrow = faLongArrowAltLeft;
constructor( constructor(
private editSongService: EditSongService, private editSongService: EditSongService,
private songsService: SongsService, private songsService: SongsService,
private change: ChangeDetectorRef private change: ChangeDetectorRef
) {} ) {
}
ngOnInit() { ngOnInit() {
this.form = this.editSongService.initSongEditForm(true); this.form = this.editSongService.initSongEditForm(true);
this.change.markForCheck(); this.change.markForCheck();
} }
public onBack(): void { public onBack(): void {
this.songsService.state = State.read; this.songsService.state = State.read;
} }
} }

View File

@@ -1,17 +1,17 @@
<form *ngIf="form"> <form *ngIf="form">
<mat-form-field> <mat-form-field>
<input <input
matInput [formControl]="form.controls.Name"
placeholder="Dateiname" matInput
[formControl]="form.controls.Name" placeholder="Dateiname"
/> />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Art</mat-label> <mat-label>Art</mat-label>
<mat-select [formControl]="form.controls.FileType"> <mat-select [formControl]="form.controls.FileType">
<mat-option *ngFor="let type of fileTypes" [value]="type.value"> <mat-option *ngFor="let type of fileTypes" [value]="type.value">
{{ type.text }} {{ type.text }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</form> </form>

View File

@@ -1,6 +1,6 @@
form { form {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
grid-column-gap: 10px; grid-column-gap: 10px;
padding-top: 10px; padding-top: 10px;
} }

View File

@@ -1,50 +1,44 @@
import { SongsService } from './../../../data/songs.service'; import {SongsService} from '../../../data/songs.service';
import { FileType } from './../../../models/files-types.model.ts'; import {FileType} from '../../../models/files-types.model.ts';
import { FormGroup } from '@angular/forms'; import {FormGroup} from '@angular/forms';
import { import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
Component, import {EditSongService} from 'src/app/data/edit-song.service';
OnInit, import {Subscription} from 'rxjs';
OnDestroy,
Input,
Output,
EventEmitter
} from '@angular/core';
import { EditSongService } from 'src/app/data/edit-song.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-song-file-edit', selector: 'app-song-file-edit',
templateUrl: './song-file-edit.component.html', templateUrl: './song-file-edit.component.html',
styleUrls: ['./song-file-edit.component.less'] styleUrls: ['./song-file-edit.component.less']
}) })
export class SongFileEditComponent implements OnInit, OnDestroy { export class SongFileEditComponent implements OnInit, OnDestroy {
@Input() fileId: number; @Input() fileId: number;
@Output() back = new EventEmitter(); @Output() back = new EventEmitter();
public form: FormGroup; public form: FormGroup;
public subscription: Subscription; public subscription: Subscription;
public fileTypes = [ public fileTypes = [
{ value: FileType.None, text: null }, {value: FileType.None, text: null},
{ value: FileType.Sheet, text: 'Text' }, {value: FileType.Sheet, text: 'Text'},
{ value: FileType.Chords, text: 'Text + Akkorde' }, {value: FileType.Chords, text: 'Text + Akkorde'},
{ value: FileType.MuseScore, text: 'MuseScore' } {value: FileType.MuseScore, text: 'MuseScore'}
]; ];
constructor( constructor(
private editSongService: EditSongService, private editSongService: EditSongService,
private songService: SongsService private songService: SongsService
) {} ) {
}
public ngOnInit(): void { public ngOnInit(): void {
const form = this.editSongService.initFileEditForm( const form = this.editSongService.initFileEditForm(
this.songService.selectedSong.value.ID, this.songService.selectedSong.value.ID,
this.fileId this.fileId
); );
this.form = form.form; this.form = form.form;
this.subscription = form.changeSubscription; this.subscription = form.changeSubscription;
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.form = null; this.form = null;
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
} }

View File

@@ -1,90 +1,90 @@
<div class="song-detail-container files" *ngIf="song"> <div *ngIf="song" class="song-detail-container files">
<mat-card class="mat-elevation-z8"> <mat-card class="mat-elevation-z8">
<mat-card-content> <mat-card-content>
<table mat-table [dataSource]="song.Files" class="mat-elevation-z8"> <table [dataSource]="song.Files" class="mat-elevation-z8" mat-table>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> <th *matHeaderCellDef mat-header-cell>
Angehängte Dateien Angehängte Dateien
</th> </th>
<td mat-cell *matCellDef="let element"> <td *matCellDef="let element" mat-cell>
<span *ngIf="fileEditId !== element.ID">{{ element.Name }}</span> <span *ngIf="fileEditId !== element.ID">{{ element.Name }}</span>
<app-song-file-edit <app-song-file-edit
[fileId]="element.ID" (back)="onClickEdit(null)"
(back)="onClickEdit(null)" *ngIf="fileEditId === element.ID"
*ngIf="fileEditId === element.ID" [fileId]="element.ID"
></app-song-file-edit> ></app-song-file-edit>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="action"> <ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef> <th *matHeaderCellDef mat-header-cell>
<input <input
type="file" #newFileUploaderRef
ng2FileSelect [uploader]="newFileUploader"
[uploader]="newFileUploader" multiple
multiple ng2FileSelect
style="display:none" style="display:none"
#newFileUploaderRef type="file"
/> />
<button <button
mat-icon-button (click)="newFileUploaderRef.click()"
(click)="newFileUploaderRef.click()" mat-icon-button
matTooltip="neue Datei hochladen" matTooltip="neue Datei hochladen"
matTooltipPosition="left" matTooltipPosition="left"
> >
<fa-icon [icon]="faFileUpload"></fa-icon> <fa-icon [icon]="faFileUpload"></fa-icon>
</button> </button>
</th> </th>
<td mat-cell *matCellDef="let element"> <td *matCellDef="let element" mat-cell>
<button <button
mat-icon-button (click)="onClickEdit(element.ID)"
(click)="onClickEdit(element.ID)" *ngIf="fileEditId !== element.ID"
matTooltip="Eintrag bearbeiten" mat-icon-button
matTooltipPosition="left" matTooltip="Eintrag bearbeiten"
*ngIf="fileEditId !== element.ID" matTooltipPosition="left"
> >
<fa-icon [icon]="faEdit"></fa-icon> <fa-icon [icon]="faEdit"></fa-icon>
</button> </button>
<button <button
mat-icon-button (click)="onClickDownload(element.ID, element.Name)"
(click)="onClickDownload(element.ID, element.Name)" *ngIf="fileEditId !== element.ID"
matTooltip="Datei herunterladen" mat-icon-button
matTooltipPosition="left" matTooltip="Datei herunterladen"
*ngIf="fileEditId !== element.ID" matTooltipPosition="left"
> >
<fa-icon [icon]="faDownload"></fa-icon> <fa-icon [icon]="faDownload"></fa-icon>
</button> </button>
<button <button
mat-icon-button (click)="onClickEdit(null)"
matTooltip="Zurück zur Tabelle" *ngIf="fileEditId === element.ID"
matTooltipPosition="left" mat-icon-button
(click)="onClickEdit(null)" matTooltip="Zurück zur Tabelle"
*ngIf="fileEditId === element.ID" matTooltipPosition="left"
> >
<fa-icon [icon]="faArrow"></fa-icon> <fa-icon [icon]="faArrow"></fa-icon>
</button> </button>
<button <button
mat-icon-button (click)="onClickDelete(element.ID)"
matTooltip="Anhang löschen" *ngIf="fileEditId === element.ID"
matTooltipPosition="left" mat-icon-button
(click)="onClickDelete(element.ID)" matTooltip="Anhang löschen"
*ngIf="fileEditId === element.ID" matTooltipPosition="left"
> >
<fa-icon [icon]="faTrash"></fa-icon> <fa-icon [icon]="faTrash"></fa-icon>
</button> </button>
</td> </td>
</ng-container> </ng-container>
<tr <tr
mat-header-row (fileOver)="onFileOverNew($event)"
*matHeaderRowDef="columns" *matHeaderRowDef="columns"
ng2FileDrop [class.file-over]="fileOverNew"
[uploader]="newFileUploader" [uploader]="newFileUploader"
(fileOver)="onFileOverNew($event)" mat-header-row
[class.file-over]="fileOverNew" ng2FileDrop
></tr> ></tr>
<tr mat-row *matRowDef="let row; columns: columns"></tr> <tr *matRowDef="let row; columns: columns" mat-row></tr>
</table> </table>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>

View File

@@ -5,11 +5,12 @@
button { button {
font-size: 24px; font-size: 24px;
color: #aaa; color: #aaa;
&:hover { &:hover {
color: #000; color: #000;
} }
} }
tr.file-over { tr.file-over {
background: #f908; background: #f908;
} }

View File

@@ -1,74 +1,71 @@
import { switchMap } from 'rxjs/operators'; import {switchMap} from 'rxjs/operators';
import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; import {ChangeDetectorRef, Component} from '@angular/core';
import { Song } from 'src/app/models/song.model'; import {Song} from 'src/app/models/song.model';
import { SongsService } from 'src/app/data/songs.service'; import {SongsService} from 'src/app/data/songs.service';
import { DownloadService } from 'src/app/data/download.service'; import {DownloadService} from 'src/app/data/download.service';
import { import {faDownload, faEdit, faFileUpload, faLongArrowAltLeft, faTrash} from '@fortawesome/free-solid-svg-icons';
faFileUpload, import {FileuploadFactory} from 'src/app/services/fileupload.factory';
faDownload, import {FileUploader} from 'ng2-file-upload';
faEdit,
faLongArrowAltLeft,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { FileuploadFactory } from 'src/app/services/fileupload.factory';
import { FileUploader } from 'ng2-file-upload';
@Component({ @Component({
selector: 'app-song-files', selector: 'app-song-files',
templateUrl: './song-files.component.html', templateUrl: './song-files.component.html',
styleUrls: ['./song-files.component.less'] styleUrls: ['./song-files.component.less']
}) })
export class SongFilesComponent { export class SongFilesComponent {
public song: Song; public song: Song;
public selectedSongId = 0; public selectedSongId = 0;
public faFileUpload = faFileUpload; public faFileUpload = faFileUpload;
public faTrash = faTrash; public faTrash = faTrash;
public faArrow = faLongArrowAltLeft; public faArrow = faLongArrowAltLeft;
public faDownload = faDownload; public faDownload = faDownload;
public faEdit = faEdit; public faEdit = faEdit;
public columns = ['name', 'action']; public columns = ['name', 'action'];
public newFileUploader: FileUploader; public newFileUploader: FileUploader;
public fileEditId: number; public fileEditId: number;
public fileOverNew = false; public fileOverNew = false;
constructor( constructor(
private downloadService: DownloadService, private downloadService: DownloadService,
private fileuploadFactory: FileuploadFactory, private fileuploadFactory: FileuploadFactory,
private songService: SongsService, private songService: SongsService,
change: ChangeDetectorRef change: ChangeDetectorRef
) { ) {
songService.selectedSong.subscribe(_ => { songService.selectedSong.subscribe(_ => {
if (_) { if (_) {
this.selectedSongId = _.ID; this.selectedSongId = _.ID;
this.song = _; this.song = _;
this.newFileUploader = this.fileuploadFactory.provideForNewFiles(_.ID); this.newFileUploader = FileuploadFactory.provideForNewFiles(_.ID);
this.newFileUploader.onCompleteItem = () => this.newFileUploader.onCompleteItem = () =>
songService.selectSong(_.ID).subscribe(); songService.selectSong(_.ID).subscribe();
this.newFileUploader.onProgressItem = () => change.markForCheck; this.newFileUploader.onProgressItem = () => change.markForCheck;
} else { } else {
this.selectedSongId = 0; this.selectedSongId = 0;
this.song = null; this.song = null;
this.newFileUploader = null; this.newFileUploader = null;
} }
change.markForCheck(); change.markForCheck();
}); });
} }
public onClickDownload(fileId: number, filename): void { public onClickDownload(fileId: number, filename): void {
this.downloadService.get(this.selectedSongId, fileId, filename); this.downloadService.get(this.selectedSongId, fileId, filename);
} }
public onFileOverNew(hover: boolean): void {
this.fileOverNew = hover; public onFileOverNew(hover: boolean): void {
} this.fileOverNew = hover;
public onClickEdit(fileId: number): void { }
this.fileEditId = fileId;
} public onClickEdit(fileId: number): void {
public onClickDelete(fileId: number): void { this.fileEditId = fileId;
const songId = this.song.ID; }
this.songService
.deleteFile$(songId, fileId) public onClickDelete(fileId: number): void {
.pipe(switchMap(() => this.songService.selectSong(songId))) const songId = this.song.ID;
.subscribe(); this.songService
} .deleteFile$(songId, fileId)
.pipe(switchMap(() => this.songService.selectSong(songId)))
.subscribe();
}
} }

View File

@@ -33,4 +33,4 @@ export const keys = [
'A#', 'A#',
'b', 'b',
'h' 'h'
]; ];

View File

@@ -1,53 +1,53 @@
<form> <form>
<mat-form-field> <mat-form-field>
<input <input
matInput [formControl]="form.controls.Name"
placeholder="Titel" matInput
[formControl]="form.controls.Name" placeholder="Titel"
/> />
</mat-form-field> </mat-form-field>
<div class="row"> <div class="row">
<mat-radio-group [formControl]="form.controls.SongType"> <mat-radio-group [formControl]="form.controls.SongType">
<mat-radio-button value="Praise">Lobpreis</mat-radio-button> <mat-radio-button value="Praise">Lobpreis</mat-radio-button>
<mat-radio-button value="Worship">Anbetung</mat-radio-button> <mat-radio-button value="Worship">Anbetung</mat-radio-button>
</mat-radio-group> </mat-radio-group>
<mat-form-field> <mat-form-field>
<input <input
matInput [formControl]="form.controls.Number"
placeholder="Nummer" matInput
[formControl]="form.controls.Number" placeholder="Nummer"
/> />
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Tonart</mat-label> <mat-label>Tonart</mat-label>
<mat-select [formControl]="form.controls.Key"> <mat-select [formControl]="form.controls.Key">
<mat-option *ngFor="let key of keys" [value]="key"> <mat-option *ngFor="let key of keys" [value]="key">
{{ key }} {{ key }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<input <input
matInput [formControl]="form.controls.Tempo"
placeholder="Tempo" matInput
[formControl]="form.controls.Tempo" placeholder="Tempo"
/> />
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field> <mat-form-field>
<textarea <textarea
matInput [formControl]="form.controls.Text"
placeholder="Liedtext" [matTextareaAutosize]="true"
[formControl]="form.controls.Text" matInput
[matTextareaAutosize]="true" placeholder="Liedtext"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<textarea <textarea
matInput [formControl]="form.controls.Comments"
placeholder="Kommentare" [matTextareaAutosize]="true"
[formControl]="form.controls.Comments" matInput
[matTextareaAutosize]="true" placeholder="Kommentare"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
</form> </form>

View File

@@ -1,9 +1,10 @@
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.row {
display: grid; .row {
grid-template-columns: 3fr 1fr 1fr 1fr; display: grid;
grid-column-gap: 20px; grid-template-columns: 3fr 1fr 1fr 1fr;
} grid-column-gap: 20px;
}
} }

View File

@@ -1,13 +1,13 @@
import { FormGroup } from '@angular/forms'; import {FormGroup} from '@angular/forms';
import { Component, Input } from '@angular/core'; import {Component, Input} from '@angular/core';
import { keys } from './keys'; import {keys} from './keys';
@Component({ @Component({
selector: 'app-song-form', selector: 'app-song-form',
templateUrl: './song-form.component.html', templateUrl: './song-form.component.html',
styleUrls: ['./song-form.component.less'] styleUrls: ['./song-form.component.less']
}) })
export class SongFormComponent { export class SongFormComponent {
@Input() public form: FormGroup; @Input() public form: FormGroup;
public keys = keys; public keys = keys;
} }

View File

@@ -1,22 +1,23 @@
<div class="song-detail-container"> <div class="song-detail-container">
<mat-card class="mat-elevation-z8" *ngIf="form"> <mat-card *ngIf="form" class="mat-elevation-z8">
<mat-card-header> <mat-card-header>
<div mat-card-avatar> <div mat-card-avatar>
<button mat-icon-button (click)="onBack()" color="warn"> <button (click)="onBack()" color="warn" mat-icon-button>
<fa-icon [icon]="faArrow"></fa-icon> <fa-icon [icon]="faArrow"></fa-icon>
</button> </button>
</div> </div>
<mat-card-title>Neuen Titel anlegen</mat-card-title> <mat-card-title>Neuen Titel anlegen</mat-card-title>
<mat-card-subtitle> <mat-card-subtitle>
</mat-card-subtitle> </mat-card-subtitle>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<app-song-form [form]="form"></app-song-form> <app-song-form [form]="form"></app-song-form>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-button (click)="onClickAdd()"> <button (click)="onClickAdd()" mat-button>
<fa-icon [icon]="faSave"></fa-icon> neu anlegen <fa-icon [icon]="faSave"></fa-icon>
</button> neu anlegen
</mat-card-actions> </button>
</mat-card-actions>
</mat-card> </mat-card>
</div> </div>

View File

@@ -1,39 +1,40 @@
import { EditSongService } from './../../../data/edit-song.service'; import {EditSongService} from '../../../data/edit-song.service';
import { FormGroup } from '@angular/forms'; import {FormGroup} from '@angular/forms';
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core';
import { faLongArrowAltLeft, faSave } from '@fortawesome/free-solid-svg-icons'; import {faLongArrowAltLeft, faSave} from '@fortawesome/free-solid-svg-icons';
import { State } from 'src/app/data/state'; import {State} from 'src/app/data/state';
import { SongsService } from 'src/app/data/songs.service'; import {SongsService} from 'src/app/data/songs.service';
@Component({ @Component({
selector: 'app-song-new', selector: 'app-song-new',
templateUrl: './song-new.component.html', templateUrl: './song-new.component.html',
styleUrls: ['./song-new.component.less'], styleUrls: ['./song-new.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SongNewComponent implements OnInit { export class SongNewComponent implements OnInit {
public faArrow = faLongArrowAltLeft; public faArrow = faLongArrowAltLeft;
public faSave = faSave; public faSave = faSave;
public form: FormGroup; public form: FormGroup;
constructor( constructor(
private editSongService: EditSongService, private editSongService: EditSongService,
private songsService: SongsService, private songsService: SongsService,
private change: ChangeDetectorRef private change: ChangeDetectorRef
) { } ) {
}
ngOnInit() { ngOnInit() {
this.form = this.editSongService.initSongEditForm(false); this.form = this.editSongService.initSongEditForm(false);
this.change.markForCheck(); this.change.markForCheck();
} }
public onBack(): void { public onBack(): void {
this.songsService.state = State.list; this.songsService.state = State.list;
this.songsService.resetSelectedSong(); this.songsService.resetSelectedSong();
} }
public onClickAdd(): void { public onClickAdd(): void {
this.songsService.saveNewSong$(this.form.value).subscribe(); this.songsService.saveNewSong$(this.form.value).subscribe();
} }
} }

View File

@@ -1,29 +1,32 @@
<div class="song-detail-container" *ngIf="song"> <div *ngIf="song" class="song-detail-container">
<mat-card class="mat-elevation-z8"> <mat-card class="mat-elevation-z8">
<mat-card-header> <mat-card-header>
<div mat-card-avatar> <div mat-card-avatar>
<button mat-icon-button (click)="onBack()" color="warn"> <button (click)="onBack()" color="warn" mat-icon-button>
<fa-icon [icon]="faArrow"></fa-icon> <fa-icon [icon]="faArrow"></fa-icon>
</button> </button>
</div> </div>
<mat-card-title>{{ song.Name }}</mat-card-title> <mat-card-title>{{ song.Name }}</mat-card-title>
<mat-card-subtitle> <mat-card-subtitle>
<mat-chip-list> <mat-chip-list>
<mat-chip *ngIf="renderSongType(song.SongType)" [style.background]="renderSongType(song.SongType).color">{{ renderSongType(song.SongType).name }}</mat-chip> <mat-chip *ngIf="renderSongType(song.SongType)"
<mat-chip *ngIf="song.Key">Tonart: {{ song.Key }}</mat-chip> [style.background]="renderSongType(song.SongType).color">{{ renderSongType(song.SongType).name }}</mat-chip>
<mat-chip *ngIf="song.Tempo">Tempo: {{ song.Tempo }}</mat-chip> <mat-chip *ngIf="song.Key">Tonart: {{ song.Key }}</mat-chip>
</mat-chip-list></mat-card-subtitle <mat-chip *ngIf="song.Tempo">Tempo: {{ song.Tempo }}</mat-chip>
> </mat-chip-list>
</mat-card-header> </mat-card-subtitle
<mat-card-content> >
<p *ngFor="let line of text">{{ line }}</p> </mat-card-header>
<br /> <mat-card-content>
<p *ngFor="let line of comments">{{ line }}</p> <p *ngFor="let line of text">{{ line }}</p>
</mat-card-content> <br/>
<mat-card-actions> <p *ngFor="let line of comments">{{ line }}</p>
<button mat-button (click)="onClickEdit()"> </mat-card-content>
<fa-icon [icon]="faEdit"></fa-icon> bearbeiten <mat-card-actions>
</button> <button (click)="onClickEdit()" mat-button>
</mat-card-actions> <fa-icon [icon]="faEdit"></fa-icon>
</mat-card> bearbeiten
</button>
</mat-card-actions>
</mat-card>
</div> </div>

View File

@@ -1,62 +1,61 @@
import { SongsService } from 'src/app/data/songs.service'; import {SongsService} from 'src/app/data/songs.service';
import { import {ChangeDetectionStrategy, ChangeDetectorRef, Component} from '@angular/core';
Component, import {faEdit, faLongArrowAltLeft} from '@fortawesome/free-solid-svg-icons';
ChangeDetectionStrategy, import {Song} from 'src/app/models/song.model';
ChangeDetectorRef import {State} from 'src/app/data/state';
} from '@angular/core';
import { faLongArrowAltLeft, faEdit } from '@fortawesome/free-solid-svg-icons';
import { Song } from 'src/app/models/song.model';
import { DownloadService } from 'src/app/data/download.service';
import { State } from 'src/app/data/state';
@Component({ @Component({
selector: 'app-song', selector: 'app-song',
templateUrl: './song.component.html', templateUrl: './song.component.html',
styleUrls: ['./song.component.less'], styleUrls: ['./song.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SongComponent { export class SongComponent {
public song: Song; public song: Song;
public faArrow = faLongArrowAltLeft; public faArrow = faLongArrowAltLeft;
public faEdit = faEdit; public faEdit = faEdit;
public selectedSongId = 0; public selectedSongId = 0;
constructor( constructor(
private songService: SongsService, private songService: SongsService,
change: ChangeDetectorRef change: ChangeDetectorRef
) { ) {
songService.selectedSong.subscribe(_ => { songService.selectedSong.subscribe(_ => {
if (_) { if (_) {
this.selectedSongId = _.ID; this.selectedSongId = _.ID;
this.song = _; this.song = _;
} else { } else {
this.selectedSongId = 0; this.selectedSongId = 0;
this.song = null; this.song = null;
} }
change.markForCheck(); change.markForCheck();
}); });
} }
public onBack(): void {
this.songService.resetSelectedSong(); public get text(): string[] {
} return this.song && this.song.Text ? this.song.Text.split(/\r?\n/) : [];
}
public onClickEdit(): void {
this.songService.state = State.edit; public get comments(): string[] {
} return this.song && this.song.Comments ? this.song.Comments.split(/\r?\n/) : [];
}
public get text(): string[] {
return this.song && this.song.Text ? this.song.Text.split(/\r?\n/) : []; public onBack(): void {
} this.songService.resetSelectedSong();
}
public get comments(): string[] {
return this.song && this.song.Comments ? this.song.Comments.split(/\r?\n/) : []; public onClickEdit(): void {
} this.songService.state = State.edit;
}
public renderSongType(songType: string) {
switch (songType) { public renderSongType(songType: string) {
case 'Praise': return {name: 'Lobpreis', color: '#99FFB8'}; switch (songType) {
case 'Worship': return {name: 'Anbetung', color: '#C999FF'}; case 'Praise':
default: return null; return {name: 'Lobpreis', color: '#99FFB8'};
case 'Worship':
return {name: 'Anbetung', color: '#C999FF'};
default:
return null;
}
} }
}
} }

View File

@@ -1,5 +1,5 @@
<app-table></app-table> <app-table></app-table>
<app-song-edit [@blend] *ngIf="songsService.state === State.edit"></app-song-edit> <app-song-edit *ngIf="songsService.state === State.edit" [@blend]></app-song-edit>
<app-song-new [@blend] *ngIf="songsService.state === State.new"></app-song-new> <app-song-new *ngIf="songsService.state === State.new" [@blend]></app-song-new>
<app-song [@blend] *ngIf="songsService.state === State.read"></app-song> <app-song *ngIf="songsService.state === State.read" [@blend]></app-song>
<app-song-files [@blend] *ngIf="songsService.state === State.read"></app-song-files> <app-song-files *ngIf="songsService.state === State.read" [@blend]></app-song-files>

View File

@@ -1,17 +1,18 @@
import { blend } from 'src/app/services/animation'; import {blend} from 'src/app/services/animation';
import { Component } from '@angular/core'; import {Component} from '@angular/core';
import { SongsService } from 'src/app/data/songs.service'; import {SongsService} from 'src/app/data/songs.service';
import { State } from 'src/app/data/state'; import {State} from 'src/app/data/state';
@Component({ @Component({
selector: 'app-songs', selector: 'app-songs',
templateUrl: './songs.component.html', templateUrl: './songs.component.html',
styleUrls: ['./songs.component.less'], styleUrls: ['./songs.component.less'],
animations: [blend] animations: [blend]
}) })
export class SongsComponent { export class SongsComponent {
public State = State; public State = State;
constructor(public songsService: SongsService) {
songsService.loadSongList$().subscribe(); constructor(public songsService: SongsService) {
} songsService.loadSongList$().subscribe();
}
} }

View File

@@ -1,75 +1,75 @@
<div <div
class="page-container mat-elevation-z8" [class.pinned]="songsService.state !== State.list"
[class.pinned]="songsService.state !== State.list" class="page-container mat-elevation-z8"
> >
<div class="table-container"> <div class="table-container">
<button <button
mat-icon-button (click)="onClickNew()"
(click)="onClickNew()" class="button-new"
class="button-new" mat-icon-button
matTooltip="neuen Titel anlegen" matTooltip="neuen Titel anlegen"
matTooltipPosition="left" matTooltipPosition="left"
> >
<fa-icon [icon]="faNew"></fa-icon> <fa-icon [icon]="faNew"></fa-icon>
</button> </button>
<table <table
mat-table [dataSource]="songsService.songs | async"
[dataSource]="songsService.songs | async" class="mat-elevation-z8"
class="mat-elevation-z8" mat-table
> >
<ng-container matColumnDef="Number"> <ng-container matColumnDef="Number">
<th mat-header-cell *matHeaderCellDef>#</th> <th *matHeaderCellDef mat-header-cell>#</th>
<td mat-cell *matCellDef="let element"> <td *matCellDef="let element" mat-cell>
<mat-chip-list> <mat-chip-list>
<mat-chip>{{ element.Number }}</mat-chip> <mat-chip>{{ element.Number }}</mat-chip>
</mat-chip-list> </mat-chip-list>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="Name"> <ng-container matColumnDef="Name">
<th mat-header-cell *matHeaderCellDef>Titel</th> <th *matHeaderCellDef mat-header-cell>Titel</th>
<td mat-cell *matCellDef="let element">{{ element.Name }}</td> <td *matCellDef="let element" mat-cell>{{ element.Name }}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="Key"> <ng-container matColumnDef="Key">
<th mat-header-cell *matHeaderCellDef></th> <th *matHeaderCellDef mat-header-cell></th>
<td mat-cell *matCellDef="let element"> <td *matCellDef="let element" mat-cell>
<mat-chip-list *ngIf="element.Key"> <mat-chip-list *ngIf="element.Key">
<mat-chip>{{ element.Key }}</mat-chip> <mat-chip>{{ element.Key }}</mat-chip>
</mat-chip-list> </mat-chip-list>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="SongType"> <ng-container matColumnDef="SongType">
<th mat-header-cell *matHeaderCellDef></th> <th *matHeaderCellDef mat-header-cell></th>
<td mat-cell *matCellDef="let element"> <td *matCellDef="let element" mat-cell>
<mat-chip-list <mat-chip-list
*ngIf="element.SongType && element.SongType !== 'None'" *ngIf="element.SongType && element.SongType !== 'None'"
> >
<mat-chip <mat-chip
[style.background-color]="renderSongType(element.SongType).color" [style.background-color]="renderSongType(element.SongType).color"
>{{ renderSongType(element.SongType).name }}</mat-chip >{{ renderSongType(element.SongType).name }}</mat-chip
> >
</mat-chip-list> </mat-chip-list>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="Tempo"> <ng-container matColumnDef="Tempo">
<th mat-header-cell *matHeaderCellDef></th> <th *matHeaderCellDef mat-header-cell></th>
<td mat-cell *matCellDef="let element"> <td *matCellDef="let element" mat-cell>
<mat-chip-list *ngIf="element.Tempo"> <mat-chip-list *ngIf="element.Tempo">
<mat-chip>{{ element.Tempo }}</mat-chip> <mat-chip>{{ element.Tempo }}</mat-chip>
</mat-chip-list> </mat-chip-list>
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr> <tr *matHeaderRowDef="columns; sticky: true" mat-header-row></tr>
<tr <tr
[class.selected]="selectedSongId === row.ID" (click)="onClick(row.ID)"
mat-row *matRowDef="let row; columns: columns"
*matRowDef="let row; columns: columns" [class.selected]="selectedSongId === row.ID"
(click)="onClick(row.ID)" mat-row
></tr> ></tr>
</table> </table>
</div> </div>
</div> </div>

View File

@@ -17,6 +17,7 @@ table {
font-size: 24px; font-size: 24px;
z-index: 1000; z-index: 1000;
color: #aaa; color: #aaa;
&:hover { &:hover {
color: #000; color: #000;
} }

View File

@@ -1,56 +1,56 @@
import { SongsService } from './../../../data/songs.service'; import {SongsService} from '../../../data/songs.service';
import { import {ChangeDetectionStrategy, ChangeDetectorRef, Component} from '@angular/core';
Component, import {State} from 'src/app/data/state';
ChangeDetectionStrategy, import {faFileMedical} from '@fortawesome/free-solid-svg-icons';
ChangeDetectorRef
} from '@angular/core';
import { State } from 'src/app/data/state';
import { faFileMedical } from '@fortawesome/free-solid-svg-icons';
@Component({ @Component({
selector: 'app-table', selector: 'app-table',
templateUrl: './table.component.html', templateUrl: './table.component.html',
styleUrls: ['./table.component.less'], styleUrls: ['./table.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TableComponent { export class TableComponent {
public selectedSongId = 0; public selectedSongId = 0;
public State = State; public State = State;
public faNew = faFileMedical; public faNew = faFileMedical;
public columnsFull = ['Number', 'Name', 'Key', 'SongType', 'Tempo']; public columnsFull = ['Number', 'Name', 'Key', 'SongType', 'Tempo'];
public columnsPinned = ['Number', 'Name']; public columnsPinned = ['Number', 'Name'];
public get columns(): string[] {
return this.songsService.state === State.list ? this.columnsFull : this.columnsPinned;
}
constructor( constructor(
public songsService: SongsService, public songsService: SongsService,
private change: ChangeDetectorRef private change: ChangeDetectorRef
) { ) {
songsService.selectedSong.subscribe(_ => { songsService.selectedSong.subscribe(_ => {
this.selectedSongId = _ ? _.ID : 0; this.selectedSongId = _ ? _.ID : 0;
this.change.markForCheck(); this.change.markForCheck();
} }
); );
}
public renderSongType(songType: string) {
switch (songType) {
case 'Praise': return {name: 'Lobpreis', color: '#99FFB8'};
case 'Worship': return {name: 'Anbetung', color: '#C999FF'};
default: return null;
} }
}
public onClick(id: number): void { public get columns(): string[] {
this.songsService.selectSong(id).subscribe(); return this.songsService.state === State.list ? this.columnsFull : this.columnsPinned;
this.change.detectChanges(); }
}
public onClickNew(): void { public renderSongType(songType: string) {
this.songsService.selectSong(null).subscribe(); switch (songType) {
this.songsService.state = State.new; case 'Praise':
this.change.detectChanges(); return {name: 'Lobpreis', color: '#99FFB8'};
} case 'Worship':
return {name: 'Anbetung', color: '#C999FF'};
default:
return null;
}
}
public onClick(id: number): void {
this.songsService.selectSong(id).subscribe();
this.change.detectChanges();
}
public onClickNew(): void {
this.songsService.selectSong(null).subscribe();
this.songsService.state = State.new;
this.change.detectChanges();
}
} }

View File

@@ -1,32 +1,33 @@
import { base } from './urls'; import {base} from './urls';
import { HttpClient } from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DownloadService { export class DownloadService {
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {
}
public get(songId: number, fileId: number, filename: string) { public get(songId: number, fileId: number, filename: string) {
return this.httpClient return this.httpClient
.get(base + '/api/songs/' + songId + '/files/' + fileId, { .get(base + '/api/songs/' + songId + '/files/' + fileId, {
responseType: 'blob' as 'json', responseType: 'blob' as 'json',
observe: 'response' observe: 'response'
}) })
.subscribe( .subscribe(
(response: any) => { (response: any) => {
const contentType = response.headers.get('Content-Type'); const contentType = response.headers.get('Content-Type');
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
const blob = new Blob([response.body], { type: contentType }); const blob = new Blob([response.body], {type: contentType});
downloadLink.href = window.URL.createObjectURL(blob); downloadLink.href = window.URL.createObjectURL(blob);
downloadLink.setAttribute('download', filename); downloadLink.setAttribute('download', filename);
document.body.appendChild(downloadLink); document.body.appendChild(downloadLink);
downloadLink.click(); downloadLink.click();
}, },
error => { error => {
console.log('download error:', JSON.stringify(error)); console.log('download error:', JSON.stringify(error));
} }
); );
} }
} }

View File

@@ -1,105 +1,106 @@
import { FileType } from './../models/files-types.model.ts'; import {SongsService} from 'src/app/data/songs.service';
import { SongsService } from 'src/app/data/songs.service'; import {Injectable} from '@angular/core';
import { Injectable } from '@angular/core'; import {FormControl, FormGroup, Validators} from '@angular/forms';
import { FormGroup, FormControl, Validators } from '@angular/forms'; import {switchMap} from 'rxjs/operators';
import { switchMap } from 'rxjs/operators'; import {Song} from '../models/song.model';
import { Song } from '../models/song.model'; import {Subscription} from 'rxjs';
import { Subscription } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class EditSongService { export class EditSongService {
constructor(private songsService: SongsService) {} constructor(private songsService: SongsService) {
public initSongEditForm(attachSync: boolean): FormGroup {
const song = attachSync
? this.songsService.selectedSong.value
: this.defaultValues();
const form = new FormGroup({
Number: new FormControl(song.Number, {
updateOn: 'blur',
validators: [Validators.required, Validators.min(1)]
}),
Name: new FormControl(song.Name, {
updateOn: 'blur',
validators: Validators.required
}),
Text: new FormControl(song.Text, { updateOn: 'blur' }),
SongType: new FormControl(song.SongType, {
updateOn: 'blur',
validators: Validators.required
}),
Key: new FormControl(song.Key, {
updateOn: 'blur',
validators: Validators.required
}),
Tempo: new FormControl(song.Tempo, { updateOn: 'blur' }),
Comments: new FormControl(song.Comments, { updateOn: 'blur' })
});
if (attachSync) {
this.attachSync(form, song);
} }
return form; public initSongEditForm(attachSync: boolean): FormGroup {
} const song = attachSync
? this.songsService.selectedSong.value
: this.defaultValues();
const form = new FormGroup({
Number: new FormControl(song.Number, {
updateOn: 'blur',
validators: [Validators.required, Validators.min(1)]
}),
Name: new FormControl(song.Name, {
updateOn: 'blur',
validators: Validators.required
}),
Text: new FormControl(song.Text, {updateOn: 'blur'}),
SongType: new FormControl(song.SongType, {
updateOn: 'blur',
validators: Validators.required
}),
Key: new FormControl(song.Key, {
updateOn: 'blur',
validators: Validators.required
}),
Tempo: new FormControl(song.Tempo, {updateOn: 'blur'}),
Comments: new FormControl(song.Comments, {updateOn: 'blur'})
});
public initFileEditForm(songId: number, fileId: number): {form: FormGroup, changeSubscription: Subscription } { if (attachSync) {
const file = this.songsService.selectedSong.value.Files.filter( this.attachSync(form, song);
_ => _.ID === fileId }
)[0];
const form = new FormGroup({
Name: new FormControl(file.Name, {
updateOn: 'blur',
validators: Validators.required
}),
FileType: new FormControl(file.FileType, {
updateOn: 'blur'
})
});
const changeSubscription = form.valueChanges.pipe( return form;
switchMap(_ => this.songsService.updateFile$(songId, fileId, _.Name, _.FileType)), }
switchMap(() => this.songsService.selectSong(songId))
).subscribe();
return {form : form, changeSubscription: changeSubscription}; public initFileEditForm(songId: number, fileId: number): { form: FormGroup, changeSubscription: Subscription } {
} const file = this.songsService.selectedSong.value.Files.filter(
_ => _.ID === fileId
)[0];
const form = new FormGroup({
Name: new FormControl(file.Name, {
updateOn: 'blur',
validators: Validators.required
}),
FileType: new FormControl(file.FileType, {
updateOn: 'blur'
})
});
private attachSync(form: FormGroup, song: Song) { const changeSubscription = form.valueChanges.pipe(
const controls = Object.keys(form.controls); switchMap(_ => this.songsService.updateFile$(songId, fileId, _.Name, _.FileType)),
controls.forEach(control => { switchMap(() => this.songsService.selectSong(songId))
form.controls[control].valueChanges ).subscribe();
.pipe(
switchMap(value => this.songsService.patch$(song.ID, control, value)),
switchMap(() => this.songsService.selectSong(song.ID))
)
.subscribe();
});
}
private defaultValues(): Song { return {form: form, changeSubscription: changeSubscription};
const song: Song = { }
ID: null,
Number: this.firstFreeNumber(),
Name: null,
Tempo: null,
Text: null,
SongType: null,
Key: null,
Comments: null,
Final: false,
Files: []
};
return song; private attachSync(form: FormGroup, song: Song) {
} const controls = Object.keys(form.controls);
controls.forEach(control => {
form.controls[control].valueChanges
.pipe(
switchMap(value => this.songsService.patch$(song.ID, control, value)),
switchMap(() => this.songsService.selectSong(song.ID))
)
.subscribe();
});
}
private firstFreeNumber(): number { private defaultValues(): Song {
let number = 0; const song: Song = {
const numbers = this.songsService.songs.value.map(_ => _.Number); ID: null,
while (numbers.indexOf(++number) !== -1) {} Number: this.firstFreeNumber(),
return number; Name: null,
} Tempo: null,
Text: null,
SongType: null,
Key: null,
Comments: null,
Final: false,
Files: []
};
return song;
}
private firstFreeNumber(): number {
let number = 0;
const numbers = this.songsService.songs.value.map(_ => _.Number);
while (numbers.indexOf(++number) !== -1) {
}
return number;
}
} }

View File

@@ -1,63 +1,63 @@
import { Song } from 'src/app/models/song.model'; import {Expand, ODataQuery, ODataService} from 'odata-v4-ng';
import { ODataService, ODataQuery, Expand } from 'odata-v4-ng'; import {Observable} from 'rxjs';
import { Observable } from 'rxjs'; import {map, tap} from 'rxjs/operators';
import { map, tap } from 'rxjs/operators'; import {base} from './urls';
import { base } from './urls';
export class OdataService { export class OdataService {
private url: string; private url: string;
constructor(private odataService: ODataService, private entity: string) {
this.url = base + '/odata/';
}
public list$<TResponse>(properties: string[]): Observable<TResponse[]> { constructor(private odataService: ODataService, private entity: string) {
const query = new ODataQuery(this.odataService, this.url) this.url = base + '/odata/';
.entitySet(this.entity) }
.select(properties);
const get = query.get().pipe(map(_ => _.toPropertyValue<TResponse[]>()));
return get; public list$<TResponse>(properties: string[]): Observable<TResponse[]> {
} const query = new ODataQuery(this.odataService, this.url)
.entitySet(this.entity)
.select(properties);
const get = query.get().pipe(map(_ => _.toPropertyValue<TResponse[]>()));
public get$<TResponse>( return get;
id: number, }
properties: string[],
expands: string[]
): Observable<TResponse> {
const query = new ODataQuery(this.odataService, this.url)
.entitySet(this.entity)
.entityKey(id)
.expand(expands.map(_ => new Expand(_)))
.select(properties);
const get = query.get().pipe(map(_ => _.toEntity<TResponse>()));
return get; public get$<TResponse>(
} id: number,
properties: string[],
expands: string[]
): Observable<TResponse> {
const query = new ODataQuery(this.odataService, this.url)
.entitySet(this.entity)
.entityKey(id)
.expand(expands.map(_ => new Expand(_)))
.select(properties);
const get = query.get().pipe(map(_ => _.toEntity<TResponse>()));
public patch$(id: number, control: string, value: any): Observable<boolean> { return get;
const valueSet = { [control]: value }; }
const query = new ODataQuery(this.odataService, this.url)
.entitySet(this.entity)
.entityKey(id);
const get = query.patch(valueSet).pipe(map(() => true));
return get; public patch$(id: number, control: string, value: any): Observable<boolean> {
} const valueSet = {[control]: value};
const query = new ODataQuery(this.odataService, this.url)
.entitySet(this.entity)
.entityKey(id);
const get = query.patch(valueSet).pipe(map(() => true));
public post$<TResponse>(values: any): Observable<TResponse> { return get;
const querry = new ODataQuery(this.odataService, this.url); }
const post = querry
.entitySet(this.entity)
.post(values)
.pipe(
tap(_ => console.log(_)),
map(_ => {
const mapped = _.toEntity<TResponse>();
return mapped;
}),
tap(_ => console.log(_))
);
return post; public post$<TResponse>(values: any): Observable<TResponse> {
} const querry = new ODataQuery(this.odataService, this.url);
const post = querry
.entitySet(this.entity)
.post(values)
.pipe(
tap(_ => console.log(_)),
map(_ => {
const mapped = _.toEntity<TResponse>();
return mapped;
}),
tap(_ => console.log(_))
);
return post;
}
} }

View File

@@ -1,124 +1,124 @@
import { HttpClient } from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import { FileType } from './../models/files-types.model.ts'; import {FileType} from './../models/files-types.model.ts';
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { ODataService } from 'odata-v4-ng'; import {ODataService} from 'odata-v4-ng';
import { OdataService } from './odata.service'; import {OdataService} from './odata.service';
import { Song } from '../models/song.model'; import {Song} from '../models/song.model';
import { BehaviorSubject, Observable } from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import { tap, switchMap } from 'rxjs/operators'; import {switchMap, tap} from 'rxjs/operators';
import { State } from './state'; import {State} from './state';
import { base } from './urls'; import {base} from './urls';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class SongsService extends OdataService { export class SongsService extends OdataService {
public state = State.list; public state = State.list;
public songs: BehaviorSubject<Song[]> = new BehaviorSubject<Song[]>([]); public songs: BehaviorSubject<Song[]> = new BehaviorSubject<Song[]>([]);
public selectedSong: BehaviorSubject<Song> = new BehaviorSubject<Song>(null); public selectedSong: BehaviorSubject<Song> = new BehaviorSubject<Song>(null);
constructor(odataService: ODataService, private httpClient: HttpClient) { constructor(odataService: ODataService, private httpClient: HttpClient) {
super(odataService, 'songs'); super(odataService, 'songs');
}
public loadSongList$(): Observable<Song[]> {
const properties = ['ID', 'Name', 'Number', 'SongType', 'Key', 'Tempo'];
const list = this.list$<Song>(properties).pipe(
tap(_ => this.songs.next(_))
);
return list;
}
public loadSongListAndGoTo$(id: number): Observable<Song> {
const properties = ['ID', 'Name', 'Number', 'SongType', 'Key', 'Tempo'];
const list = this.list$<Song>(properties).pipe(
tap(_ => {
this.songs.next(_);
}),
switchMap(() => this.selectSong(id))
);
return list;
}
public selectSong(id: number): Observable<Song> {
this.state = State.read;
const filter = this.songs.value.filter(_ => _.ID === id);
const song = filter.length === 1 ? filter[0] : null;
if (!song) {
return;
} }
const get = this.get$<Song>(id, ['Text', 'Comments'], ['Files']).pipe(tap(_ => { public loadSongList$(): Observable<Song[]> {
song.Text = _.Text; const properties = ['ID', 'Name', 'Number', 'SongType', 'Key', 'Tempo'];
song.Comments = _.Comments; const list = this.list$<Song>(properties).pipe(
song.Files = _.Files; tap(_ => this.songs.next(_))
this.selectedSong.next(song); );
})); return list;
}
return get; public loadSongListAndGoTo$(id: number): Observable<Song> {
} const properties = ['ID', 'Name', 'Number', 'SongType', 'Key', 'Tempo'];
const list = this.list$<Song>(properties).pipe(
tap(_ => {
this.songs.next(_);
}),
switchMap(() => this.selectSong(id))
);
public resetSelectedSong() { return list;
this.state = State.list; }
this.selectedSong.next(null);
}
public patch$(id: number, control: string, value: any): Observable<boolean> { public selectSong(id: number): Observable<Song> {
const patch = super.patch$(id, control, value).pipe( this.state = State.read;
tap(() => { const filter = this.songs.value.filter(_ => _.ID === id);
const songs = this.songs.value; const song = filter.length === 1 ? filter[0] : null;
const song = songs.filter(_ => _.ID === id)[0]; if (!song) {
song[control] = value; return;
this.songs.next(songs); }
this.selectedSong.next(song);
})
);
return patch; const get = this.get$<Song>(id, ['Text', 'Comments'], ['Files']).pipe(tap(_ => {
} song.Text = _.Text;
song.Comments = _.Comments;
song.Files = _.Files;
this.selectedSong.next(song);
}));
public saveNewSong$(values: any): Observable<Song> { return get;
const newSong = super }
.post$<Song>(values)
.pipe(switchMap(_ => this.loadSongListAndGoTo$(_.ID)));
return newSong; public resetSelectedSong() {
} this.state = State.list;
this.selectedSong.next(null);
}
public updateFile$( public patch$(id: number, control: string, value: any): Observable<boolean> {
songId: number, const patch = super.patch$(id, control, value).pipe(
fileId: number, tap(() => {
name: string, const songs = this.songs.value;
fileType: FileType const song = songs.filter(_ => _.ID === id)[0];
): Observable<any> { song[control] = value;
const url = this.songs.next(songs);
base + this.selectedSong.next(song);
'/api/songs/' + })
songId + );
'/files/' +
fileId +
'/edit?Name=' +
name +
'&FileType=' +
fileType;
const get = this.httpClient.get(url);
return get;
}
public deleteFile$( return patch;
songId: number, }
fileId: number
): Observable<any> { public saveNewSong$(values: any): Observable<Song> {
const url = const newSong = super
base + .post$<Song>(values)
'/api/songs/' + .pipe(switchMap(_ => this.loadSongListAndGoTo$(_.ID)));
songId +
'/files/' + return newSong;
fileId + }
'/delete';
const get = this.httpClient.get(url); public updateFile$(
return get; songId: number,
} fileId: number,
name: string,
fileType: FileType
): Observable<any> {
const url =
base +
'/api/songs/' +
songId +
'/files/' +
fileId +
'/edit?Name=' +
name +
'&FileType=' +
fileType;
const get = this.httpClient.get(url);
return get;
}
public deleteFile$(
songId: number,
fileId: number
): Observable<any> {
const url =
base +
'/api/songs/' +
songId +
'/files/' +
fileId +
'/delete';
const get = this.httpClient.get(url);
return get;
}
} }

View File

@@ -1,6 +1,6 @@
export enum State { export enum State {
read, read,
edit, edit,
new, new,
list list
} }

View File

@@ -1,6 +1,7 @@
import { FileType } from './files-types.model.ts'; import {FileType} from './files-types.model.ts';
export interface File { export interface File {
ID: number; ID: number;
Name: string; Name: string;
FileType: FileType; FileType: FileType;
} }

View File

@@ -1,6 +1,6 @@
export enum FileType { export enum FileType {
None = 'None', None = 'None',
Sheet = 'Sheet', Sheet = 'Sheet',
Chords = 'Chords', Chords = 'Chords',
MuseScore = 'MuseScore' MuseScore = 'MuseScore'
} }

View File

@@ -1,14 +1,14 @@
import { File } from './file.model'; import {File} from './file.model';
export interface Song { export interface Song {
ID: number; ID: number;
Number: number; Number: number;
Name: string; Name: string;
Text: string; Text: string;
Comments: string; Comments: string;
Key: string; Key: string;
Tempo: number; Tempo: number;
SongType: string; SongType: string;
Final: boolean; Final: boolean;
Files: File[]; Files: File[];
} }

View File

@@ -1,13 +1,13 @@
import { trigger, transition, style, animate } from '@angular/animations'; import {animate, style, transition, trigger} from '@angular/animations';
export const blend = trigger('blend', [ export const blend = trigger('blend', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0, display: 'none' }), style({opacity: 0, display: 'none'}),
animate('400ms', style({ opacity: 0 , display: 'none'})), animate('400ms', style({opacity: 0, display: 'none'})),
animate('300ms', style({ opacity: 1, display: 'block' })) animate('300ms', style({opacity: 1, display: 'block'}))
]), ]),
transition(':leave', [ transition(':leave', [
style({ opacity: 1, display: 'block' }), style({opacity: 1, display: 'block'}),
animate('300ms', style({ opacity: 0, display: 'none' })) animate('300ms', style({opacity: 0, display: 'none'}))
]) ])
]); ]);

View File

@@ -1,18 +1,18 @@
import { base } from './../data/urls'; import {base} from '../data/urls';
import { Injectable } from '@angular/core'; import {Injectable} from '@angular/core';
import { FileUploader } from 'ng2-file-upload'; import {FileUploader} from 'ng2-file-upload';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class FileuploadFactory { export class FileuploadFactory {
public provideForNewFiles(songId: number): FileUploader { public static provideForNewFiles(songId: number): FileUploader {
const uploader = new FileUploader({ const uploader = new FileUploader({
url: base + '/api/songs/' + songId + '/files', url: base + '/api/songs/' + songId + '/files',
autoUpload: true, autoUpload: true,
isHTML5: true isHTML5: true
}); });
return uploader; return uploader;
} }
} }

View File

@@ -1,3 +1,3 @@
export const environment = { export const environment = {
production: true production: true
}; };

View File

@@ -3,7 +3,7 @@
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
export const environment = { export const environment = {
production: false production: false
}; };
/* /*

View File

@@ -1,14 +1,14 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Wgenerator</title> <title>Wgenerator</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta content="width=device-width, initial-scale=1" name="viewport">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link href="favicon.ico" rel="icon" type="image/x-icon">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@@ -2,30 +2,30 @@
// https://karma-runner.github.io/1.0/config/configuration-file.html // https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) { module.exports = function (config) {
config.set({ config.set({
basePath: '', basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'], frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [ plugins: [
require('karma-jasmine'), require('karma-jasmine'),
require('karma-chrome-launcher'), require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'), require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'), require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma') require('@angular-devkit/build-angular/plugins/karma')
], ],
client: { client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser clearContext: false // leave Jasmine Spec Runner output visible in browser
}, },
coverageIstanbulReporter: { coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'), dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'], reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true fixWebpackSourcePaths: true
}, },
reporters: ['progress', 'kjhtml'], reporters: ['progress', 'kjhtml'],
port: 9876, port: 9876,
colors: true, colors: true,
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
autoWatch: true, autoWatch: true,
browsers: ['Chrome'], browsers: ['Chrome'],
singleRun: false singleRun: false
}); });
}; };

View File

@@ -1,12 +1,12 @@
import { enableProdMode } from '@angular/core'; import {enableProdMode} from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module'; import {AppModule} from './app/app.module';
import { environment } from './environments/environment'; import {environment} from './environments/environment';
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule) platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err)); .catch(err => console.error(err));

View File

@@ -59,20 +59,20 @@
* user can disable parts of macroTask/DomEvents patch by setting following flags * user can disable parts of macroTask/DomEvents patch by setting following flags
*/ */
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/* /*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge * with the following flag, it will bypass `zone.js` patch for IE/Edge
*/ */
// (window as any).__Zone_enable_cross_context_check = true; // (window as any).__Zone_enable_cross_context_check = true;
/*************************************************************************************************** /***************************************************************************************************
* Zone JS is required by default for Angular itself. * Zone JS is required by default for Angular itself.
*/ */
import 'zone.js/dist/zone'; // Included with Angular CLI. import 'zone.js/dist/zone'; // Included with Angular CLI.
/*************************************************************************************************** /***************************************************************************************************

View File

@@ -19,17 +19,21 @@ html {
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: 8px;
transition: all 300ms ease-in-out; transition: all 300ms ease-in-out;
.mat-table tbody { .mat-table tbody {
background: none; background: none;
} }
th.mat-header-cell:first-of-type { th.mat-header-cell:first-of-type {
border-top-left-radius: 8px; border-top-left-radius: 8px;
transition: all 300ms ease-in-out; transition: all 300ms ease-in-out;
} }
th.mat-header-cell:last-of-type { th.mat-header-cell:last-of-type {
border-top-right-radius: 8px; border-top-right-radius: 8px;
transition: all 300ms ease-in-out; transition: all 300ms ease-in-out;
} }
.mat-table thead { .mat-table thead {
border-top-right-radius: 8px; border-top-right-radius: 8px;
border-top-left-radius: 8px; border-top-left-radius: 8px;
@@ -40,13 +44,16 @@ html {
tr.selected { tr.selected {
background-color: #0002; background-color: #0002;
} }
tr:hover { tr:hover {
cursor: pointer; cursor: pointer;
background-color: #0001; background-color: #0001;
td { td {
color: #ff9900; color: #ff9900;
} }
} }
td.mat-cell { td.mat-cell {
padding: 0 5px; padding: 0 5px;
} }
@@ -58,14 +65,17 @@ html {
bottom: 0; bottom: 0;
right: 70vw; right: 70vw;
border-radius: 0px; border-radius: 0px;
th.mat-header-cell:first-of-type { th.mat-header-cell:first-of-type {
border-top-left-radius: 0px; border-top-left-radius: 0px;
transition: all 300ms ease-in-out; transition: all 300ms ease-in-out;
} }
th.mat-header-cell:last-of-type { th.mat-header-cell:last-of-type {
border-top-right-radius: 0px; border-top-right-radius: 0px;
transition: all 300ms ease-in-out; transition: all 300ms ease-in-out;
} }
.mat-table thead { .mat-table thead {
border-top-right-radius: 0px; border-top-right-radius: 0px;
border-top-left-radius: 0px; border-top-left-radius: 0px;
@@ -95,9 +105,11 @@ html {
.song-detail-container { .song-detail-container {
margin-left: 30vw; margin-left: 30vw;
.mat-form-field-infix { .mat-form-field-infix {
width: 80px; width: 80px;
} }
.mat-radio-button { .mat-radio-button {
margin: 15px 10px 0 10px; margin: 15px 10px 0 10px;
} }
@@ -106,15 +118,17 @@ html {
.mat-card { .mat-card {
padding: 0; padding: 0;
} }
table { table {
width: 100%; width: 100%;
background: none; background: none;
box-shadow: none; box-shadow: none;
} }
td.mat-cell:last-of-type, td.mat-footer-cell:last-of-type, th.mat-header-cell:last-of-type { td.mat-cell:last-of-type, td.mat-footer-cell:last-of-type, th.mat-header-cell:last-of-type {
padding-right: 8px; padding-right: 8px;
text-align: right; text-align: right;
} }
} }
} }

View File

@@ -1,18 +1,15 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files // This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing'; import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing'; import {getTestBed} from '@angular/core/testing';
import { import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any; declare const require: any;
// First, initialize the Angular testing environment. // First, initialize the Angular testing environment.
getTestBed().initTestEnvironment( getTestBed().initTestEnvironment(
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting() platformBrowserDynamicTesting()
); );
// Then we find all the tests. // Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/); const context = require.context('./', true, /\.spec\.ts$/);

View File

@@ -1,17 +1,17 @@
{ {
"extends": "../tslint.json", "extends": "../tslint.json",
"rules": { "rules": {
"directive-selector": [ "directive-selector": [
true, true,
"attribute", "attribute",
"app", "app",
"camelCase" "camelCase"
], ],
"component-selector": [ "component-selector": [
true, true,
"element", "element",
"app", "app",
"kebab-case" "kebab-case"
] ]
} }
} }