refine list

This commit is contained in:
Benjamin Ifland
2019-03-24 14:37:29 +01:00
parent 1cb8a119b9
commit af4493ec94
23 changed files with 268 additions and 231 deletions

2
.vscode/launch.json vendored
View File

@@ -9,7 +9,7 @@
"request": "launch", "request": "launch",
"name": "Launch Chrome against localhost", "name": "Launch Chrome against localhost",
"url": "http://localhost:4200", "url": "http://localhost:4200",
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}/WEB"
} }
] ]
} }

15
WEB/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@@ -1,24 +1,11 @@
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 { Routes, RouterModule } from '@angular/router';
import { SongsResolverService } from './data/songs-resolver.service';
import { SongComponent } from './components/song/song.component';
import { SongResolverService } from './data/song-resolver.service';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'songs', path: 'songs',
component: SongsComponent, component: SongsComponent
resolve: {
songs: SongsResolverService
}
},
{
path: 'songs/:id',
component: SongComponent,
resolve: {
song: SongResolverService
}
}, },
{ {
path: '', path: '',

View File

@@ -1,2 +1,3 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
<router-outlet name="detail"></router-outlet>

View File

@@ -5,6 +5,4 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.less'] styleUrls: ['./app.component.less']
}) })
export class AppComponent { export class AppComponent { }
title = 'wgenerator';
}

View File

@@ -1,4 +1,5 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ODataModule } from 'odata-v4-ng'; import { ODataModule } from 'odata-v4-ng';
@@ -10,15 +11,17 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
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 { 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/song/song.component'; import { SongComponent } from './components/songs/song/song.component';
@NgModule({ @NgModule({
declarations: [AppComponent, SongsComponent, TableComponent, SongComponent], declarations: [AppComponent, SongsComponent, TableComponent, SongComponent],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule,
AppRoutingModule, AppRoutingModule,
HttpClientModule, HttpClientModule,
ODataModule, ODataModule,
@@ -26,6 +29,7 @@ import { SongComponent } from './components/song/song.component';
MatCardModule, MatCardModule,
MatTableModule, MatTableModule,
MatButtonModule, MatButtonModule,
MatChipsModule,
FontAwesomeModule FontAwesomeModule
], ],

View File

@@ -1,17 +0,0 @@
<mat-card class="mat-elevation-z8">
<mat-card-header>
<div mat-card-avatar>
<button mat-icon-button [routerLink]="['/songs']" >
<fa-icon [icon]="faArrow"></fa-icon>
</button>
</div>
<mat-card-title>{{ song.Name }}</mat-card-title>
<mat-card-subtitle>{{ song.Key }} - {{ song.Tempo }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p *ngFor="let line of text">{{ line }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="onClickDownload()">Herunterladen</button>
</mat-card-actions>
</mat-card>

View File

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

View File

@@ -1,35 +0,0 @@
import { DownloadService } from "./../../data/download.service";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { faLongArrowAltLeft } from "@fortawesome/free-solid-svg-icons";
import { Song } from "src/app/models/song.model";
@Component({
selector: "app-song",
templateUrl: "./song.component.html",
styleUrls: ["./song.component.less"]
})
export class SongComponent implements OnInit {
public song: Song;
public faArrow = faLongArrowAltLeft;
constructor(
private route: ActivatedRoute,
private downloadService: DownloadService
) {}
ngOnInit() {
this.route.data.subscribe((data: { song: Song }) => {
this.song = data.song;
});
}
public onClickDownload(): void {
const id = this.song.ID;
this.downloadService.get(id, false);
}
public get text(): string[] {
return this.song.Text.split(/\r?\n/).filter(_ => _ !== ' ');
}
}

View File

@@ -0,0 +1,19 @@
<div class="song-detail-container">
<mat-card class="mat-elevation-z8" [@blend] *ngIf="selectedSongId !== 0">
<mat-card-header>
<div mat-card-avatar>
<button mat-icon-button (click)="onBack()">
<fa-icon [icon]="faArrow"></fa-icon>
</button>
</div>
<mat-card-title>{{ song.Name }}</mat-card-title>
<mat-card-subtitle>{{ song.Key }} - {{ song.Tempo }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p *ngFor="let line of text">{{ line }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="onClickDownload()">Herunterladen</button>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -1,15 +1,22 @@
.mat-card { .mat-card {
width: 500px; width: 500px;
border-radius: 8px; border-radius: 8px;
background: #fffe;
margin: 20px;
box-sizing: border-box;
} }
.mat-card-title { .mat-card-title {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 450px; width: 420px;
} }
.mat-card-content { .mat-card-content {
white-space: pre-wrap; white-space: pre-wrap;
} }
.song-detail-container {
margin-left: 30vw;
}

View File

@@ -0,0 +1,64 @@
import { SongsService } from 'src/app/data/songs.service';
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons';
import { Song } from 'src/app/models/song.model';
import { DownloadService } from 'src/app/data/download.service';
import { trigger, transition, style, animate } from '@angular/animations';
@Component({
selector: 'app-song',
templateUrl: './song.component.html',
styleUrls: ['./song.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('blend', [
transition(':enter', [
style({ opacity: 0 }),
animate('700ms', style({ opacity: 0 })),
animate('300ms', style({ opacity: 1 }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate('300ms', style({ opacity: 0 }))
])
])
]
})
export class SongComponent {
public song: Song;
public faArrow = faLongArrowAltLeft;
public selectedSongId = 0;
constructor(
private songService: SongsService,
private downloadService: DownloadService,
change: ChangeDetectorRef
) {
songService.selectedSong.subscribe(_ => {
if (_) {
this.selectedSongId = _.ID;
this.song = _;
} else {
this.selectedSongId = 0;
this.song = null;
}
change.markForCheck();
});
}
public onBack(): void {
this.songService.resetSelectedSong();
}
public onClickDownload(): void {
const id = this.song.ID;
this.downloadService.get(id, false);
}
public get text(): string[] {
return this.song.Text.split(/\r?\n/).filter(_ => _ !== ' ');
}
}

View File

@@ -1 +1,2 @@
<app-table [songs]="songs"></app-table> <app-table></app-table>
<app-song></app-song>

View File

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

View File

@@ -1,22 +1,13 @@
import { Song } from './../../models/song.model'; import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core'; import { SongsService } from 'src/app/data/songs.service';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'app-songs', selector: 'app-songs',
templateUrl: './songs.component.html', templateUrl: './songs.component.html',
styleUrls: ['./songs.component.less'] styleUrls: ['./songs.component.less']
}) })
export class SongsComponent implements OnInit { export class SongsComponent {
public songs: Song[]; constructor(songService: SongsService) {
songService.loadSongList();
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.data.subscribe((data: { songs: Song[] }) => {
this.songs = data.songs;
});
} }
} }

View File

@@ -1,9 +1,11 @@
<div class="page-container mat-elevation-z8"> <div
class="page-container mat-elevation-z8"
[class.pinned]="selectedSongId !== 0"
>
<div class="table-container"> <div class="table-container">
<table <table
*ngIf="songs"
mat-table mat-table
[dataSource]="songs" [dataSource]="songsService.songs | async"
class="mat-elevation-z8" class="mat-elevation-z8"
> >
<ng-container matColumnDef="Number"> <ng-container matColumnDef="Number">
@@ -18,25 +20,37 @@
<ng-container matColumnDef="Key"> <ng-container matColumnDef="Key">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">{{ element.Key }}</td> <td mat-cell *matCellDef="let element">
<mat-chip-list *ngIf="element.Key">
<mat-chip>{{ element.Key }}</mat-chip>
</mat-chip-list>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="SongType"> <ng-container matColumnDef="SongType">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">{{ element.SongType }}</td> <td mat-cell *matCellDef="let element">
<mat-chip-list *ngIf="element.SongType && element.SongType!=='None'">
<mat-chip [style.background-color]="renderSongType(element.SongType).color">{{ renderSongType(element.SongType).name }}</mat-chip>
</mat-chip-list>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="Tempo"> <ng-container matColumnDef="Tempo">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">{{ element.Tempo }}</td> <td mat-cell *matCellDef="let element">
<mat-chip-list *ngIf="element.Tempo">
<mat-chip>{{ element.Tempo }}</mat-chip>
</mat-chip-list>
</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr> <tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr <tr
[class.selected]="selectedSongId === row.ID"
mat-row mat-row
*matRowDef="let row; columns: columns" *matRowDef="let row; columns: columns"
[routerLink]="['/songs', row.ID]" (click)="onClick(row.ID)"
routerLinkActive="router-link-active"
></tr> ></tr>
</table> </table>
</div> </div>

View File

@@ -1,15 +1,13 @@
table { table {
width: 100%; width: 100%;
border-radius: 8px; border-radius: 8px;
//background: #fffe;
} }
.page-container {
height: 400px;
overflow: hidden;
border-radius: 8px;
}
.table-container { .table-container {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }

View File

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

View File

@@ -1,26 +1,45 @@
import { Component, OnInit, Input } from '@angular/core'; import { SongsService } from './../../../data/songs.service';
import { Song } from 'src/app/models/song.model'; import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
@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
}) })
export class TableComponent implements OnInit { export class TableComponent {
@Input() public songs: Song[]; public selectedSongId = 0;
public columns = [ public columnsFull = ['Number', 'Name', 'Key', 'SongType', 'Tempo'];
'Number', public columnsPinned = ['Number', 'Name'];
'Name', public get columns(): string[] {
'Key', return this.selectedSongId === 0 ? this.columnsFull : this.columnsPinned;
'SongType',
'Tempo',
];
constructor() { }
ngOnInit() {
console.log(this.songs);
} }
constructor(
public songsService: SongsService,
private change: ChangeDetectorRef
) {
songsService.selectedSong.subscribe(_ => {
this.selectedSongId = _ ? _.ID : 0;
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 {
this.songsService.selectSong(id);
this.change.detectChanges();
}
} }

View File

@@ -1,20 +0,0 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Song } from '../models/song.model';
import { SongsService } from './songs.service';
@Injectable({
providedIn: 'root'
})
export class SongResolverService implements Resolve<Song> {
constructor(private songsService: SongsService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Song> {
const id = route.params.id;
const get$ = this.songsService.get<Song>(id);
return get$;
}
}

View File

@@ -1,18 +0,0 @@
import { Song } from './../models/song.model';
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { SongsService } from './songs.service';
@Injectable({
providedIn: 'root'
})
export class SongsResolverService implements Resolve<Song[]> {
constructor(private songsService: SongsService) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Song[]> {
const get$ = this.songsService.list<Song>();
return get$;
}
}

View File

@@ -1,11 +1,31 @@
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 { BehaviorSubject } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class SongsService extends OdataService { export class SongsService extends OdataService {
public songs: BehaviorSubject<Song[]> = new BehaviorSubject<Song[]>([]);
public selectedSong: BehaviorSubject<Song> = new BehaviorSubject<Song>(null);
constructor(odataService: ODataService) { constructor(odataService: ODataService) {
super(odataService, 'songs'); super(odataService, 'songs');
} }
public loadSongList(): void {
this.list<Song>().subscribe(_ => this.songs.next(_));
}
public selectSong(id: number): void {
const filter = this.songs.value.filter(_ => _.ID === id);
const song = filter.length === 1 ? filter[0] : null;
this.selectedSong.next(song);
}
public resetSelectedSong() {
this.selectedSong.next(null);
}
} }

View File

@@ -1,11 +1,75 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; @import "~@angular/material/prebuilt-themes/indigo-pink.css";
tbody {
body {
margin: 0px;
}
html {
background-image: url(https://images.unsplash.com/photo-1476136236990-838240be4859?ixlib=rb-1.2.1&auto=format&fit=crop&w=2167&q=80);
}
.page-container {
position: absolute;
top: 20px;
left: 20px;
bottom: 20px;
right: 20px;
box-sizing: border-box;
overflow: hidden;
border-radius: 8px;
transition: all 300ms ease-in-out;
.mat-table tbody {
background: none;
}
th.mat-header-cell:first-of-type {
border-top-left-radius: 8px;
transition: all 300ms ease-in-out;
}
th.mat-header-cell:last-of-type {
border-top-right-radius: 8px;
transition: all 300ms ease-in-out;
}
.mat-table thead {
border-top-right-radius: 8px;
border-top-left-radius: 8px;
transition: all 300ms ease-in-out;
}
tr.selected {
background-color: #0002;
}
tr:hover { tr:hover {
cursor: pointer; cursor: pointer;
background-color: #0001;
td { td {
color: #ff9900; color: #ff9900;
} }
} }
td.mat-cell {
padding: 0 5px;
}
&.pinned {
top: 0;
left: 0;
bottom: 0;
right: 70vw;
border-radius: 0px;
th.mat-header-cell:first-of-type {
border-top-left-radius: 0px;
transition: all 300ms ease-in-out;
}
th.mat-header-cell:last-of-type {
border-top-right-radius: 0px;
transition: all 300ms ease-in-out;
}
.mat-table thead {
border-top-right-radius: 0px;
border-top-left-radius: 0px;
transition: all 300ms ease-in-out;
}
}
} }