mirror of
https://gitlab.com/openlp/web-remote.git
synced 2024-12-22 19:32:49 +00:00
New api and more views
This commit is contained in:
parent
911cbbb901
commit
163344f423
@ -23,11 +23,10 @@
|
|||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "src/tsconfig.app.json",
|
"tsConfig": "src/tsconfig.app.json",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
|
||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@ -76,7 +75,7 @@
|
|||||||
"tsConfig": "src/tsconfig.spec.json",
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
"karmaConfig": "src/karma.conf.js",
|
"karmaConfig": "src/karma.conf.js",
|
||||||
"styles": [
|
"styles": [
|
||||||
"styles.css"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"assets": [
|
"assets": [
|
||||||
|
64
package.json
64
package.json
@ -20,41 +20,41 @@
|
|||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^6.0.0",
|
"@angular/animations": "^8.2.8",
|
||||||
"@angular/cdk": "^6.4.2",
|
"@angular/cdk": "^8.2.1",
|
||||||
"@angular/common": "^6.0.0",
|
"@angular/common": "^8.2.8",
|
||||||
"@angular/compiler": "^6.0.0",
|
"@angular/compiler": "^8.2.8",
|
||||||
"@angular/core": "^6.0.0",
|
"@angular/core": "^8.2.8",
|
||||||
"@angular/forms": "^6.0.0",
|
"@angular/forms": "^8.2.8",
|
||||||
"@angular/http": "^6.0.0",
|
"@angular/material": "^8.2.1",
|
||||||
"@angular/material": "^6.4.2",
|
"@angular/platform-browser": "^8.2.8",
|
||||||
"@angular/platform-browser": "^6.0.0",
|
"@angular/platform-browser-dynamic": "^8.2.8",
|
||||||
"@angular/platform-browser-dynamic": "^6.0.0",
|
"@angular/router": "^8.2.8",
|
||||||
"@angular/router": "^6.0.0",
|
"core-js": "^3.2.1",
|
||||||
"core-js": "^2.5.4",
|
"hammerjs": "^2.0.8",
|
||||||
"rxjs": "^6.0.0",
|
"rxjs": "^6.5.3",
|
||||||
"zone.js": "^0.8.26"
|
"zone.js": "^0.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.6.0",
|
"@angular-devkit/build-angular": "~0.803.6",
|
||||||
"@angular/cli": "~6.0.0",
|
"@angular/cli": "~8.3.6",
|
||||||
"@angular/compiler-cli": "^6.0.0",
|
"@angular/compiler-cli": "^8.2.8",
|
||||||
"@angular/language-service": "^6.0.0",
|
"@angular/language-service": "^8.2.8",
|
||||||
"@types/jasmine": "~2.8.6",
|
"@types/jasmine": "~3.4.1",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.7",
|
||||||
"@types/node": "~8.9.4",
|
"@types/node": "~12.7.8",
|
||||||
"codelyzer": "~4.2.1",
|
"codelyzer": "~5.1.2",
|
||||||
"jasmine-core": "~2.99.1",
|
"jasmine-core": "~3.5.0",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
"karma": "~1.7.1",
|
"karma": "~4.3.0",
|
||||||
"karma-chrome-launcher": "~2.2.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "~1.4.2",
|
"karma-coverage-istanbul-reporter": "~2.1.0",
|
||||||
"karma-jasmine": "~1.1.1",
|
"karma-jasmine": "~2.0.1",
|
||||||
"karma-jasmine-html-reporter": "^0.2.2",
|
"karma-jasmine-html-reporter": "^1.4.2",
|
||||||
"protractor": "~5.3.0",
|
"protractor": "~5.4.2",
|
||||||
"ts-node": "~5.0.1",
|
"ts-node": "~8.4.1",
|
||||||
"tslint": "~5.9.1",
|
"tslint": "~5.20.0",
|
||||||
"typescript": "~2.7.2"
|
"typescript": "~3.5.0"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
<a mat-list-item (click)="menu.close()" routerLink="/alerts">Alerts</a>
|
<a mat-list-item (click)="menu.close()" routerLink="/alerts">Alerts</a>
|
||||||
<a mat-list-item (click)="menu.close()" routerLink="/search">Search</a>
|
<a mat-list-item (click)="menu.close()" routerLink="/search">Search</a>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
<a mat-list-item (click)="menu.close()" routerLink="/main">Main View</a>
|
||||||
|
<a mat-list-item (click)="menu.close()" routerLink="/stage">Stage View</a>
|
||||||
|
<a mat-list-item (click)="menu.close()" routerLink="/chords">Chord View</a>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
<mat-slide-toggle color="primary" [checked]="fastSwitching" (change)="sliderChanged($event)">Fast switching</mat-slide-toggle>
|
<mat-slide-toggle color="primary" [checked]="fastSwitching" (change)="sliderChanged($event)">Fast switching</mat-slide-toggle>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
@ -14,6 +18,8 @@
|
|||||||
<mat-toolbar style="background-color: #64aef3;">
|
<mat-toolbar style="background-color: #64aef3;">
|
||||||
<button mat-icon-button (click)="menu.toggle()"><mat-icon>menu</mat-icon></button>
|
<button mat-icon-button (click)="menu.toggle()"><mat-icon>menu</mat-icon></button>
|
||||||
<span>OpenLP Remote</span>
|
<span>OpenLP Remote</span>
|
||||||
|
<span class="filler"></span>
|
||||||
|
<button *ngIf="showLogin" mat-button (click)="login()">Login</button>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
</header>
|
</header>
|
||||||
<main class="content">
|
<main class="content">
|
||||||
|
@ -11,6 +11,10 @@ mat-sidenav {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filler {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,40 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { State } from './responses';
|
import { State } from './responses';
|
||||||
import { OpenLPService } from './openlp.service';
|
import { OpenLPService } from './openlp.service';
|
||||||
import { MatSlideToggleChange } from '@angular/material';
|
import { MatSlideToggleChange, MatDialog } from '@angular/material';
|
||||||
|
import { LoginComponent } from './components/login/login.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
fastSwitching = false;
|
fastSwitching = false;
|
||||||
state: State = new State();
|
state = new State();
|
||||||
|
showLogin = false;
|
||||||
|
|
||||||
constructor(private openlpService: OpenLPService) {
|
constructor(private openlpService: OpenLPService, private dialog: MatDialog) {
|
||||||
openlpService.stateChanged$.subscribe(item => this.state = item);
|
openlpService.stateChanged$.subscribe(item => this.state = item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.openlpService.retrieveSystemInformation().subscribe(res => this.showLogin = res.login_required);
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
const dialogRef = this.dialog.open(LoginComponent, {
|
||||||
|
width: '250px'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.showLogin = false;
|
||||||
|
this.openlpService.setAuthToken(result.token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
nextItem() {
|
nextItem() {
|
||||||
this.openlpService.nextItem().subscribe();
|
this.openlpService.nextItem().subscribe();
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
|||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { MatCardModule, MatDialogModule, MatSnackBarModule } from '@angular/material';
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatListModule } from '@angular/material/list';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@ -25,10 +26,21 @@ import { AlertComponent } from './components/alert/alert.component';
|
|||||||
import { SearchComponent } from './components/search/search.component';
|
import { SearchComponent } from './components/search/search.component';
|
||||||
import { SlidesComponent } from './components/slides/slides.component';
|
import { SlidesComponent } from './components/slides/slides.component';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ChordViewComponent } from './components/chord-view/chord-view.component';
|
||||||
|
import { StageViewComponent } from './components/stage-view/stage-view.component';
|
||||||
|
import { MainViewComponent } from './components/main-view/main-view.component';
|
||||||
|
import { ChordProPipe } from './components/chord-view/chordpro.pipe';
|
||||||
|
import { LoginComponent } from './components/login/login.component';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
ChordViewComponent,
|
||||||
|
StageViewComponent,
|
||||||
|
MainViewComponent,
|
||||||
|
ChordProPipe,
|
||||||
|
LoginComponent,
|
||||||
ServiceComponent,
|
ServiceComponent,
|
||||||
AlertComponent,
|
AlertComponent,
|
||||||
SearchComponent,
|
SearchComponent,
|
||||||
@ -50,11 +62,17 @@ import { FormsModule } from '@angular/forms';
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatSlideToggleModule
|
MatSlideToggleModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatSnackBarModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
OpenLPService
|
OpenLPService
|
||||||
],
|
],
|
||||||
|
entryComponents: [
|
||||||
|
LoginComponent
|
||||||
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
@ -1,39 +1,27 @@
|
|||||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
import { ServiceComponent } from './components/service/service.component';
|
import { ServiceComponent } from './components/service/service.component';
|
||||||
import { AlertComponent } from './components/alert/alert.component';
|
import { AlertComponent } from './components/alert/alert.component';
|
||||||
import { SearchComponent } from './components/search/search.component';
|
import { SearchComponent } from './components/search/search.component';
|
||||||
import { SlidesComponent } from './components/slides/slides.component';
|
import { SlidesComponent } from './components/slides/slides.component';
|
||||||
|
import { ChordViewComponent } from './components/chord-view/chord-view.component';
|
||||||
|
import { MainViewComponent } from './components/main-view/main-view.component';
|
||||||
|
import { StageViewComponent } from './components/stage-view/stage-view.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{ path: '', redirectTo: '/service', pathMatch: 'full' },
|
||||||
path: '',
|
{ path: 'service', component: ServiceComponent },
|
||||||
redirectTo: '/service',
|
{ path: 'slides', component: SlidesComponent },
|
||||||
pathMatch: 'full'
|
{ path: 'alerts', component: AlertComponent },
|
||||||
},
|
{ path: 'search', component: SearchComponent },
|
||||||
{
|
{ path: 'chords', component: ChordViewComponent },
|
||||||
path: 'service',
|
{ path: 'main', component: MainViewComponent },
|
||||||
component: ServiceComponent
|
{ path: 'stage', component: StageViewComponent }
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'slides',
|
|
||||||
component: SlidesComponent
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'alerts',
|
|
||||||
component: AlertComponent
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'search',
|
|
||||||
component: SearchComponent
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes)],
|
imports: [RouterModule.forRoot(routes, {useHash: true})],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class AppRoutingModule {
|
export class AppRoutingModule { }
|
||||||
}
|
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input matInput [(ngModel)]="alert" type="text" name="alert" placeholder="Alert" required>
|
<input matInput [(ngModel)]="alert" type="text" name="alert" placeholder="Alert" required>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="primary" id="sendButton" [disabled]="!alertForm.form.valid" (click)="onSubmit()">Send</button>
|
<button mat-raised-button color="primary" id="sendButton" [disabled]="!alertForm.form.valid" (click)="onSubmit(); alertForm.reset()">Send</button>
|
||||||
</form>
|
</form>
|
@ -2,6 +2,7 @@ import { Component } from '@angular/core';
|
|||||||
|
|
||||||
|
|
||||||
import { OpenLPService } from '../../openlp.service';
|
import { OpenLPService } from '../../openlp.service';
|
||||||
|
import { MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'openlp-alert',
|
selector: 'openlp-alert',
|
||||||
@ -14,11 +15,9 @@ export class AlertComponent {
|
|||||||
|
|
||||||
public alert: string;
|
public alert: string;
|
||||||
|
|
||||||
constructor(private openlpService: OpenLPService) { }
|
constructor(private openlpService: OpenLPService, private snackBar: MatSnackBar) { }
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
console.log('submitted: ', this.alert);
|
this.openlpService.showAlert(this.alert).subscribe(res => this.snackBar.open('Alert submitted', '', {duration: 2000}));
|
||||||
this.openlpService.showAlert(this.alert).subscribe(res => console.log(res));
|
|
||||||
this.alert = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
src/app/components/chord-view/chord-view.component.html
Normal file
26
src/app/components/chord-view/chord-view.component.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<div class="overlay">
|
||||||
|
<div>
|
||||||
|
<div class="tags">
|
||||||
|
<span *ngFor="let tag of tags" [class.active]="tag.active">{{ tag.text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="slide currentSlide song" [innerHTML]="chordproFormatted(currentSlides[0])|chordpro:transpose"></div>
|
||||||
|
<div class="nextSlides">
|
||||||
|
<div class="slide song" [class.first]="slide.first_slide_of_tag" *ngFor="let slide of nextSlides" [innerHTML]="chordproFormatted(slide)|chordpro:transpose"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="time">{{ time|date:'HH:mm' }}</div>
|
||||||
|
<div class="transpose">
|
||||||
|
<button mat-icon-button (click)="transposeUp()">
|
||||||
|
<mat-icon>keyboard_arrow_up</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span>{{ transpose }}</span>
|
||||||
|
<button mat-icon-button (click)="transposeDown()">
|
||||||
|
<mat-icon>keyboard_arrow_down</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button routerLink="/">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
12
src/app/components/chord-view/chord-view.component.scss
Normal file
12
src/app/components/chord-view/chord-view.component.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.transpose {
|
||||||
|
margin-left: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 3rem;
|
||||||
|
mat-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin-left: 17px;
|
||||||
|
}
|
||||||
|
}
|
38
src/app/components/chord-view/chord-view.component.ts
Normal file
38
src/app/components/chord-view/chord-view.component.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
|
import { OpenLPService } from '../../openlp.service';
|
||||||
|
import { Slide } from '../../responses';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { StageViewComponent } from '../stage-view/stage-view.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chord-view',
|
||||||
|
templateUrl: './chord-view.component.html',
|
||||||
|
styleUrls: ['./chord-view.component.scss', '../overlay.scss', './chordpro.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None // needed for the chords to be displayed
|
||||||
|
|
||||||
|
})
|
||||||
|
export class ChordViewComponent extends StageViewComponent {
|
||||||
|
transpose = 0;
|
||||||
|
|
||||||
|
transposeUp(): void {
|
||||||
|
this.transpose++;
|
||||||
|
}
|
||||||
|
|
||||||
|
transposeDown(): void {
|
||||||
|
this.transpose--;
|
||||||
|
}
|
||||||
|
|
||||||
|
chordproFormatted(slide: Slide): string {
|
||||||
|
if (!slide) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let chordpro: string = slide.chords_text;
|
||||||
|
chordpro = chordpro.replace(/<span class="\w*\s*\w*">/g, '');
|
||||||
|
chordpro = chordpro.replace(/<span>/g, '');
|
||||||
|
chordpro = chordpro.replace(/<\/span>/g, '');
|
||||||
|
chordpro = chordpro.replace(/<strong>/g, '[');
|
||||||
|
chordpro = chordpro.replace(/<\/strong>/g, ']');
|
||||||
|
|
||||||
|
return chordpro;
|
||||||
|
}
|
||||||
|
}
|
183
src/app/components/chord-view/chordpro.pipe.ts
Normal file
183
src/app/components/chord-view/chordpro.pipe.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* ChordProPipe
|
||||||
|
*
|
||||||
|
* A pipe for angular 2/4 that translate ChordPro-formatted text into an HTML representation, to be used in conjunction with a set of styles
|
||||||
|
* for proper display.
|
||||||
|
*
|
||||||
|
* If you make improvements, please send them to me for incorporation.
|
||||||
|
*
|
||||||
|
* @author David Quinn-Jacobs (dqj@authentrics.com)
|
||||||
|
* @licence Use this in any way you like, with no constraints.
|
||||||
|
*/
|
||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY } from '@angular/material';
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@Pipe({ name: 'chordpro' })
|
||||||
|
|
||||||
|
export class ChordProPipe implements PipeTransform {
|
||||||
|
/**
|
||||||
|
* @var chordRegex Expression used to determine if given line contains a chord.
|
||||||
|
* @type {RegExp}
|
||||||
|
*/
|
||||||
|
private chordRegex = /\[([^\]]*)\]/;
|
||||||
|
private readonly MAX_HALF_STEPS = 11;
|
||||||
|
|
||||||
|
constructor(private sanitizer: DomSanitizer) {
|
||||||
|
this.notesSharpNotation['german'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H'];
|
||||||
|
this.notesFlatNotation['german'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H'];
|
||||||
|
this.notesSharpNotation['english'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||||
|
this.notesFlatNotation['english'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private keys = [
|
||||||
|
{ name: 'Ab', value: 0 },
|
||||||
|
{ name: 'A', value: 1 },
|
||||||
|
{ name: 'Bb', value: 2 },
|
||||||
|
{ name: 'A#', value: 2 },
|
||||||
|
{ name: 'B', value: 3 },
|
||||||
|
{ name: 'C', value: 4 },
|
||||||
|
{ name: 'C#', value: 5 },
|
||||||
|
{ name: 'Db', value: 5 },
|
||||||
|
{ name: 'D', value: 6 },
|
||||||
|
{ name: 'Eb', value: 7 },
|
||||||
|
{ name: 'D#', value: 7 },
|
||||||
|
{ name: 'E', value: 8 },
|
||||||
|
{ name: 'F', value: 9 },
|
||||||
|
{ name: 'F#', value: 10 },
|
||||||
|
{ name: 'Gb', value: 10 },
|
||||||
|
{ name: 'G', value: 11 },
|
||||||
|
{ name: 'G#', value: 0 }
|
||||||
|
];
|
||||||
|
notesSharpNotation = {};
|
||||||
|
notesFlatNotation = {};
|
||||||
|
|
||||||
|
decodeHTML(value: string) {
|
||||||
|
const tempElement = document.createElement('div');
|
||||||
|
tempElement.innerHTML = value;
|
||||||
|
return tempElement.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipe transformation for ChordPro-formatted song texts.
|
||||||
|
* @param {string} song
|
||||||
|
* @param {number} nHalfSteps
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
transform(song: string, nHalfSteps: number): string|SafeHtml {
|
||||||
|
try {
|
||||||
|
if (song !== undefined && song) {
|
||||||
|
return this.sanitizer.bypassSecurityTrustHtml(this.parseToHTML(song, nHalfSteps));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (exception) {
|
||||||
|
console.warn('chordpro translation error', exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chordRoot(chord) {
|
||||||
|
let root = '';
|
||||||
|
let ch2 = '';
|
||||||
|
if (chord && chord.length > 0) {
|
||||||
|
root = chord.substr(0, 1);
|
||||||
|
if (chord.length > 1) {
|
||||||
|
ch2 = chord.substr(1, 1);
|
||||||
|
if (ch2 === 'b' || ch2 === '#') {
|
||||||
|
root += ch2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
restOfChord(chord) {
|
||||||
|
let rest = '';
|
||||||
|
const root = this.chordRoot(chord);
|
||||||
|
if (chord.length > root.length) {
|
||||||
|
rest = chord.substr(root.length);
|
||||||
|
}
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transpose the given chord the given (positive or negative) number of half steps.
|
||||||
|
* @param {string} chordRoot
|
||||||
|
* @param {number} nHalfSteps
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
transposeChord(chordRoot, nHalfSteps) {
|
||||||
|
let pos = -1;
|
||||||
|
for (let i = 0; i < this.keys.length; i++) {
|
||||||
|
if (this.keys[i].name === chordRoot) {
|
||||||
|
pos = this.keys[i].value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos >= 0) {
|
||||||
|
pos += nHalfSteps;
|
||||||
|
if (pos < 0) {
|
||||||
|
pos += this.MAX_HALF_STEPS;
|
||||||
|
}
|
||||||
|
else if (pos > this.MAX_HALF_STEPS) {
|
||||||
|
pos -= this.MAX_HALF_STEPS + 1;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < this.keys.length; i++) {
|
||||||
|
if (this.keys[i].value === pos) {
|
||||||
|
return this.keys[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chordRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a string containing a ChordPro-formatted song, building an array of output HTML lines.
|
||||||
|
*
|
||||||
|
* @param {number} nHalfSteps
|
||||||
|
* @param {string} song
|
||||||
|
*/
|
||||||
|
private parseToHTML(song: string, nHalfSteps = 0): string {
|
||||||
|
// we are currently receiving html, we need to replace that stuff,
|
||||||
|
// becuase it gets messed up when a chord is placed on it..
|
||||||
|
// shouldn't be relevant if we actually get chordpro format
|
||||||
|
song = this.decodeHTML(song);
|
||||||
|
const comp = this;
|
||||||
|
if (!song) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let chordText = '';
|
||||||
|
let lastChord = '';
|
||||||
|
if (!song.match(comp.chordRegex)) {
|
||||||
|
return `<div class="no-chords">${song}</div>`;
|
||||||
|
}
|
||||||
|
song.split(comp.chordRegex).forEach((part, index) => {
|
||||||
|
if (index % 2 === 0) {
|
||||||
|
// text
|
||||||
|
if (lastChord) {
|
||||||
|
chordText += `<span data-chord="${lastChord}">${part.substring(0, 1)}</span>${part.substring(1)}`;
|
||||||
|
lastChord = '';
|
||||||
|
} else {
|
||||||
|
chordText += part;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// chord
|
||||||
|
lastChord = part.replace(/[[]]/, '');
|
||||||
|
if (nHalfSteps !== 0) {
|
||||||
|
lastChord = lastChord.split('/').map(chord => {
|
||||||
|
const chordRoot = comp.chordRoot(chord);
|
||||||
|
const newRoot = comp.transposeChord(chordRoot, nHalfSteps);
|
||||||
|
return newRoot + comp.restOfChord(chord);
|
||||||
|
}).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// use proper symbols
|
||||||
|
lastChord = lastChord.replace(/b/g, '♭');
|
||||||
|
lastChord = lastChord.replace(/#/g, '♯');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return `<div class="with-chords">${chordText}</div>`;
|
||||||
|
}
|
||||||
|
}
|
23
src/app/components/chord-view/chordpro.scss
Normal file
23
src/app/components/chord-view/chordpro.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.song {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
.with-chords {
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
span[data-chord]:before {
|
||||||
|
position: relative;
|
||||||
|
top: -1em;
|
||||||
|
display: inline-block;
|
||||||
|
content: attr(data-chord);
|
||||||
|
width: 0;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextSlides {
|
||||||
|
.song {
|
||||||
|
span[data-chord]:before {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/app/components/login/login.component.html
Normal file
15
src/app/components/login/login.component.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<h1 mat-dialog-title>Login</h1>
|
||||||
|
|
||||||
|
<form #loginForm="ngForm">
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput placeholder="Username" [(ngModel)]="username" name="username" required>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput placeholder="password" type="password" [(ngModel)]="password" name="password" required>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-raised-button id="loginButton" color="primary" [disabled]="!loginForm.form.valid" (click)="performLogin()">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
3
src/app/components/login/login.component.scss
Normal file
3
src/app/components/login/login.component.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
29
src/app/components/login/login.component.ts
Normal file
29
src/app/components/login/login.component.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Credentials } from '../../responses';
|
||||||
|
import { MatDialogRef, MatSnackBar } from '@angular/material';
|
||||||
|
import { OpenLPService } from '../../openlp.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss']
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnInit {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
constructor(private dialogRef: MatDialogRef<LoginComponent>, private openlpService: OpenLPService,
|
||||||
|
private snackBar: MatSnackBar) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
performLogin() {
|
||||||
|
this.openlpService.login({username: this.username, password: this.password}).subscribe(
|
||||||
|
result => {
|
||||||
|
this.snackBar.open('Successfully logged in', '', {duration: 2000});
|
||||||
|
this.dialogRef.close(result);
|
||||||
|
},
|
||||||
|
err => this.snackBar.open('Login failed', '', {duration: 2000})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
3
src/app/components/main-view/main-view.component.html
Normal file
3
src/app/components/main-view/main-view.component.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div class="overlay">
|
||||||
|
<img src="{{ img }}">
|
||||||
|
</div>
|
8
src/app/components/main-view/main-view.component.scss
Normal file
8
src/app/components/main-view/main-view.component.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
22
src/app/components/main-view/main-view.component.ts
Normal file
22
src/app/components/main-view/main-view.component.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { OpenLPService } from '../../openlp.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-main-view',
|
||||||
|
templateUrl: './main-view.component.html',
|
||||||
|
styleUrls: ['./main-view.component.scss', '../overlay.scss']
|
||||||
|
})
|
||||||
|
export class MainViewComponent implements OnInit {
|
||||||
|
img: string;
|
||||||
|
constructor(private openlpService: OpenLPService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.updateImage();
|
||||||
|
this.openlpService.stateChanged$.subscribe(item => this.updateImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImage(): void {
|
||||||
|
this.openlpService.getMainImage().subscribe(view => this.img = view.binary_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
64
src/app/components/overlay.scss
Normal file
64
src/app/components/overlay.scss
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
.overlay {
|
||||||
|
background: black;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
margin: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: gray;
|
||||||
|
font-size: 4rem;
|
||||||
|
span {
|
||||||
|
margin-left: 1rem;
|
||||||
|
&.active {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
font-size: 3rem;
|
||||||
|
white-space: pre-line;
|
||||||
|
margin: 0;
|
||||||
|
&.first {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextSlides {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: gray;
|
||||||
|
.slide {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
@ -27,11 +27,11 @@ export class SearchComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendLive(id) {
|
sendLive(id) {
|
||||||
this.openlpService.sendItemLive(this.currentPlugin, id).subscribe(res => console.log(res));
|
this.openlpService.sendItemLive(this.currentPlugin, id).subscribe(res => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
addToService(id) {
|
addToService(id) {
|
||||||
this.openlpService.addItemToService(this.currentPlugin, id).subscribe(res => console.log(res));
|
this.openlpService.addItemToService(this.currentPlugin, id).subscribe(res => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
@ -18,7 +18,7 @@ export class ServiceComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onItemSelected(item) {
|
onItemSelected(item) {
|
||||||
this.openlpService.setServiceItem(item).subscribe(res => console.log(res));
|
this.openlpService.setServiceItem(item).subscribe(res => {});
|
||||||
this.router.navigate(['slides']);
|
this.router.navigate(['slides']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export class SlidesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSlideSelected(item) {
|
onSlideSelected(item) {
|
||||||
this.openlpService.setSlide(item).subscribe(res => console.log(res));
|
this.openlpService.setSlide(item).subscribe(res => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSlides() {
|
getSlides() {
|
||||||
|
21
src/app/components/stage-view/stage-view.component.html
Normal file
21
src/app/components/stage-view/stage-view.component.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<div class="overlay">
|
||||||
|
<div>
|
||||||
|
<div class="tags">
|
||||||
|
<span *ngFor="let tag of tags" [class.active]="tag.active">{{ tag.text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="slide currentSlide mat-display-3">
|
||||||
|
{{ currentSlides[activeSlide]?.text }}
|
||||||
|
</div>
|
||||||
|
<div class="nextSlides">
|
||||||
|
<div class="slide mat-display-1" [class.first]="slide.first_slide_of_tag" *ngFor="let slide of nextSlides">
|
||||||
|
{{ slide.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="time">{{ time|date:'HH:mm' }}</div>
|
||||||
|
<button mat-raised-button class="closeButton" routerLink="/">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
84
src/app/components/stage-view/stage-view.component.ts
Normal file
84
src/app/components/stage-view/stage-view.component.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { OpenLPService } from '../../openlp.service';
|
||||||
|
import { Slide } from '../../responses';
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
text: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-stage-view',
|
||||||
|
templateUrl: './stage-view.component.html',
|
||||||
|
styleUrls: ['./stage-view.component.scss', '../overlay.scss']
|
||||||
|
})
|
||||||
|
export class StageViewComponent implements OnInit {
|
||||||
|
currentSlides: Slide[] = [];
|
||||||
|
activeSlide = 0;
|
||||||
|
tags: Tag[] = [];
|
||||||
|
time = new Date();
|
||||||
|
constructor(private openlpService: OpenLPService) {
|
||||||
|
setInterval(() => this.time = new Date(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.updateCurrentSlides();
|
||||||
|
this.openlpService.stateChanged$.subscribe(item => this.updateCurrentSlides());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentSlides(): void {
|
||||||
|
this.openlpService.getItemSlides().subscribe(slides => this.setNewSlides(slides));
|
||||||
|
}
|
||||||
|
|
||||||
|
get nextSlides(): Slide[] {
|
||||||
|
return this.currentSlides.slice(this.activeSlide + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewSlides(slides: Slide[]): void {
|
||||||
|
if (slides.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentSlides = slides;
|
||||||
|
this.activeSlide = slides.findIndex(s => s.selected);
|
||||||
|
this.updateTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method updates the tags from the current slides.
|
||||||
|
*
|
||||||
|
* We add a tag as soon as we know we need it.
|
||||||
|
* So we start with the first tag and on each tag change we push the new one.
|
||||||
|
*
|
||||||
|
* If we find the same tag, we check to see if the current slide is a repition.
|
||||||
|
* In case of a repition we also add a new tag.
|
||||||
|
*
|
||||||
|
* TODO This approach should work for most cases. It is a primary candidate for a test :-)
|
||||||
|
*/
|
||||||
|
updateTags(): void {
|
||||||
|
this.tags = [];
|
||||||
|
this.tags.push({text: this.currentSlides[0].tag, active: this.currentSlides[0].selected});
|
||||||
|
let lastIndex = 0;
|
||||||
|
loop:
|
||||||
|
for (let index = 1; index < this.currentSlides.length; ++index) {
|
||||||
|
let foundActive = false;
|
||||||
|
if (this.currentSlides[index].tag === this.currentSlides[lastIndex].tag) {
|
||||||
|
for (let i = 0; i < index - lastIndex; ++i) {
|
||||||
|
foundActive = foundActive || this.currentSlides[index + i].selected;
|
||||||
|
|
||||||
|
// they are different, stop checking and continue outer loop
|
||||||
|
if (this.currentSlides[lastIndex + i].text !== this.currentSlides[index + i].text) {
|
||||||
|
// Since we are collapsing tags, we make sure to mark the tag active, if any of the collapsed tags were active
|
||||||
|
if (foundActive) {
|
||||||
|
this.tags[this.tags.length - 1].active = foundActive;
|
||||||
|
}
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// either the tags differed, or we found a repitition. Either way add a tag
|
||||||
|
this.tags.push({text: this.currentSlides[index].tag, active: this.currentSlides[index].selected});
|
||||||
|
this.currentSlides[index].first_slide_of_tag = true;
|
||||||
|
lastIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
import { Injectable, EventEmitter } from '@angular/core';
|
import { Injectable, EventEmitter } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { URLSearchParams, Http } from '@angular/http';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
|
||||||
import { PluginDescription, State, Slide, ServiceItem } from './responses';
|
import { PluginDescription, State, Slide, ServiceItem, MainView, SystemInformation, Credentials, AuthToken } from './responses';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
const deserialize = (json, cls) => {
|
const deserialize = (json, cls) => {
|
||||||
@ -18,6 +17,10 @@ const deserialize = (json, cls) => {
|
|||||||
return inst;
|
return inst;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const httpOptions = {
|
||||||
|
headers: new HttpHeaders({'Content-Type': 'application/json'})
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OpenLPService {
|
export class OpenLPService {
|
||||||
private apiURL: string;
|
private apiURL: string;
|
||||||
@ -31,30 +34,45 @@ export class OpenLPService {
|
|||||||
else {
|
else {
|
||||||
port = '4316';
|
port = '4316';
|
||||||
}
|
}
|
||||||
this.apiURL = `http://localhost:${port}`;
|
this.apiURL = `http://localhost:${port}/api/v1`;
|
||||||
|
|
||||||
|
|
||||||
this.stateChanged$ = new EventEmitter<State>();
|
this.stateChanged$ = new EventEmitter<State>();
|
||||||
let state: State = null;
|
this.retrieveSystemInformation().subscribe(info => {
|
||||||
const ws: WebSocket = new WebSocket('ws://localhost:4317/state');
|
const ws = new WebSocket(`ws://localhost:${info.websocket_port}/state`);
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
state = deserialize(JSON.parse(reader.result).results, State);
|
const state = deserialize(JSON.parse(reader.result as string).results, State);
|
||||||
this.stateChanged$.emit(state);
|
this.stateChanged$.emit(state);
|
||||||
};
|
};
|
||||||
reader.readAsText(event.data);
|
reader.readAsText(event.data);
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthToken(token: string): void {
|
||||||
|
httpOptions.headers = httpOptions.headers.set('Authorization', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieveSystemInformation(): Observable<SystemInformation> {
|
||||||
|
return this.http.get<SystemInformation>(`${this.apiURL}/core/system`, httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMainImage(): Observable<MainView> {
|
||||||
|
return this.http.get<MainView>(`${this.apiURL}/core/live-image`, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemSlides(): Observable<Slide[]> {
|
getItemSlides(): Observable<Slide[]> {
|
||||||
return this.http.get<Slide[]>(`${this.apiURL}/controller/live/text`);
|
return this.http.get<Slide[]>(`${this.apiURL}/controller/live-item`, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getServiceItems(): Observable<ServiceItem[]> {
|
getServiceItems(): Observable<ServiceItem[]> {
|
||||||
return this.http.get<ServiceItem[]>(`${this.apiURL}/service/list`);
|
return this.http.get<ServiceItem[]>(`${this.apiURL}/service/list`, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchablePlugins(): Observable<PluginDescription[]> {
|
getSearchablePlugins(): Observable<PluginDescription[]> {
|
||||||
return this.http.get<PluginDescription[]>(`${this.apiURL}/plugin/search`);
|
return this.http.get<PluginDescription[]>(`${this.apiURL}/core/plugins`, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
setServiceItem(id: number): Observable<any> {
|
setServiceItem(id: number): Observable<any> {
|
||||||
@ -62,54 +80,58 @@ export class OpenLPService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(plugin, text): Observable<any> {
|
search(plugin, text): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/${plugin}/search?q=${text}`);
|
return this.http.get(`${this.apiURL}/plugins/${plugin}/search?text=${text}`, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSlide(id): Observable<any> {
|
setSlide(id): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/controller/live/set?id=${id}`);
|
return this.http.post(`${this.apiURL}/controller/show`, {'id': id}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextItem(): Observable<any> {
|
nextItem(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/service/next`);
|
return this.http.post(`${this.apiURL}/service/progress`, {'action': 'next'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousItem(): Observable<any> {
|
previousItem(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/service/previous`);
|
return this.http.post(`${this.apiURL}/service/progress`, {'action': 'previous'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSlide(): Observable<any> {
|
nextSlide(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/controller/live/next`);
|
return this.http.post(`${this.apiURL}/controller/progress`, {'action': 'next'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousSlide(): Observable<any> {
|
previousSlide(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/controller/live/previous`);
|
return this.http.post(`${this.apiURL}/controller/progress`, {'action': 'previous'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
blankDisplay(): Observable<any> {
|
blankDisplay(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/display/blank`);
|
return this.http.post(`${this.apiURL}/core/display`, {'display': 'blank'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
themeDisplay(): Observable<any> {
|
themeDisplay(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/display/theme`);
|
return this.http.post(`${this.apiURL}/core/display`, {'display': 'theme'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
desktopDisplay(): Observable<any> {
|
desktopDisplay(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/display/desktop`);
|
return this.http.post(`${this.apiURL}/core/display`, {'display': 'desktop'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDisplay(): Observable<any> {
|
showDisplay(): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/display/show`);
|
return this.http.post(`${this.apiURL}/core/display`, {'display': 'show'}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlert(text): Observable<any> {
|
showAlert(text): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/alert?text=${text}`);
|
return this.http.post(`${this.apiURL}/plugins/alerts`, {'text': text}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendItemLive(plugin, id): Observable<any> {
|
sendItemLive(plugin, id): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/${plugin}/live?id=${id}`);
|
return this.http.post(`${this.apiURL}/plugins/${plugin}/live`, {'id': id}, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
addItemToService(plugin, id): Observable<any> {
|
addItemToService(plugin, id): Observable<any> {
|
||||||
return this.http.get(`${this.apiURL}/${plugin}/add?id=${id}`);
|
return this.http.post(`${this.apiURL}/plugins/${plugin}/add`, {'id': id}, httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
login(credentials: Credentials): Observable<AuthToken> {
|
||||||
|
return this.http.post<AuthToken>(`${this.apiURL}/core/login`, credentials, httpOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,9 @@ export interface Slide {
|
|||||||
html: string;
|
html: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
chords_text: string;
|
||||||
lines: string[];
|
lines: string[];
|
||||||
|
first_slide_of_tag: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceItem {
|
export interface ServiceItem {
|
||||||
@ -31,3 +33,21 @@ export interface ServiceItem {
|
|||||||
selected: boolean;
|
selected: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MainView {
|
||||||
|
binary_image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemInformation {
|
||||||
|
websocket_port: number;
|
||||||
|
login_required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Credentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthToken {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
BIN
src/assets/favicon.ico
Normal file
BIN
src/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB |
@ -6,14 +6,14 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root>
|
<app-root>
|
||||||
<div style="margin-top: 80px;">
|
<div style="margin-top: 80px;">
|
||||||
<div style="margin: auto; width: 100%; display: block;">
|
<div style="margin: auto; width: 100%; display: block;">
|
||||||
<img src="/assets/images/loading.png" style='height: 100%; width: 100%; object-fit: contain'>
|
<img src="/assets/loading.png" style='height: 100%; width: 100%; object-fit: contain'>
|
||||||
<p style="text-align: center;">Loading...</p>
|
<p style="text-align: center;">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@ 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';
|
||||||
|
import 'hammerjs';
|
||||||
|
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
/** Evergreen browsers require these. **/
|
/** Evergreen browsers require these. **/
|
||||||
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
||||||
import 'core-js/es7/reflect';
|
// import 'core-js/es7/reflect';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
|
||||||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
|
||||||
|
|
||||||
|
|
||||||
* {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-sidenav-layout {
|
|
||||||
background: rgba(0,0,0,0.03);
|
|
||||||
}
|
|
||||||
mat-sidenav {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.displayButton .active {
|
|
||||||
background: 'teal';
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
45
src/styles.scss
Normal file
45
src/styles.scss
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||||
|
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-sidenav-layout {
|
||||||
|
background: rgba(0,0,0,0.03);
|
||||||
|
}
|
||||||
|
mat-sidenav {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayButton .active {
|
||||||
|
background: 'teal';
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline1 {
|
||||||
|
line-height: 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chordline span.chord span strong {
|
||||||
|
position: absolute;
|
||||||
|
top: -2.1rem;
|
||||||
|
left: 0;
|
||||||
|
font-size: 30pt;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: normal;
|
||||||
|
color: yellow;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user