Update the UI

This commit is contained in:
Raoul Snyman 2019-11-07 18:02:26 +00:00
parent f3ffb599a9
commit 642e0ebf04
16 changed files with 204 additions and 138 deletions

View File

@ -1,4 +1,10 @@
<mat-sidenav-container class="all-wrap" fullscreen> <mat-toolbar color="primary">
<mat-toolbar-row>
<button mat-icon-button (click)="menu.toggle()"><mat-icon>menu</mat-icon></button>
<span class="page-title">{{pageTitle}}</span>
</mat-toolbar-row>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #menu mode="over"> <mat-sidenav #menu mode="over">
<mat-nav-list> <mat-nav-list>
<a mat-list-item (click)="menu.close()" routerLink="/service">Service</a> <a mat-list-item (click)="menu.close()" routerLink="/service">Service</a>
@ -13,51 +19,43 @@
<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>
<div class="page-wrap"> <mat-sidenav-content>
<header> <main class="content">
<mat-toolbar style="background-color: #64aef3;"> <router-outlet></router-outlet>
<button mat-icon-button (click)="menu.toggle()"><mat-icon>menu</mat-icon></button> </main>
<span>OpenLP Remote</span> <footer>
<span class="filler"></span> <mat-toolbar class="footer">
<button *ngIf="showLogin" mat-button (click)="login()">Login</button> <button mat-icon-button (click)="previousItem()" matTooltip="Previous item">
</mat-toolbar> <mat-icon>first_page</mat-icon>
</header> </button>
<main class="content"> <button mat-icon-button (click)="nextItem()" matTooltip="Next item">
<router-outlet></router-outlet> <mat-icon>last_page</mat-icon>
</main> </button>
<footer> <button mat-icon-button (click)="previousSlide()" matTooltip="Previous slide">
<mat-toolbar class="footer"> <mat-icon>navigate_before</mat-icon>
<button mat-icon-button (click)="previousItem()" matTooltip="Previous item"> </button>
<mat-icon>first_page</mat-icon> <button mat-icon-button (click)="nextSlide()" matTooltip="Next slide">
</button> <mat-icon>navigate_next</mat-icon>
<button mat-icon-button (click)="nextItem()" matTooltip="Next item"> </button>
<mat-icon>last_page</mat-icon> <button mat-icon-button (click)="blankDisplay()" class="displayButton" [class.active]="state.blank" [disabled]="state.blank" matTooltip="Show black">
</button> <mat-icon>videocam_off</mat-icon>
<button mat-icon-button (click)="previousSlide()" matTooltip="Previous slide"> </button>
<mat-icon>navigate_before</mat-icon> <button mat-icon-button (click)="themeDisplay()" class="displayButton" [class.active]="state.theme" [disabled]="state.theme" matTooltip="Show background">
</button> <mat-icon>wallpaper</mat-icon>
<button mat-icon-button (click)="nextSlide()" matTooltip="Next slide"> </button>
<mat-icon>navigate_next</mat-icon> <button mat-icon-button (click)="desktopDisplay()" class="displayButton" [class.active]="state.display" [disabled]="state.display" matTooltip="Show Desktop">
</button> <mat-icon>desktop_windows</mat-icon>
<button mat-icon-button (click)="blankDisplay()" class="displayButton" [class.active]="state.blank" [disabled]="state.blank" matTooltip="Show black"> </button>
<mat-icon>videocam_off</mat-icon> <button mat-icon-button (click)="showDisplay()" class="displayButton" [class.active]="state.display" [disabled]="state.live()" matTooltip="Show Presentation">
</button> <mat-icon>videocam</mat-icon>
<button mat-icon-button (click)="themeDisplay()" class="displayButton" [class.active]="state.theme" [disabled]="state.theme" matTooltip="Show background"> </button>
<mat-icon>wallpaper</mat-icon>
</button>
<button mat-icon-button (click)="desktopDisplay()" class="displayButton" [class.active]="state.display" [disabled]="state.display" matTooltip="Show Desktop">
<mat-icon>desktop_windows</mat-icon>
</button>
<button mat-icon-button (click)="showDisplay()" class="displayButton" [class.active]="state.display" [disabled]="state.live()" matTooltip="Show Presentation">
<mat-icon>videocam</mat-icon>
</button>
</mat-toolbar>
<mat-toolbar *ngIf="fastSwitching" class="fast-access">
<button mat-icon-button routerLink="/service"><mat-icon>list</mat-icon></button>
<button mat-icon-button routerLink="/slides"><mat-icon>collections</mat-icon></button>
<button mat-icon-button routerLink="/alerts"><mat-icon>error</mat-icon></button>
<button mat-icon-button routerLink="/search"><mat-icon>search</mat-icon></button>
</mat-toolbar> </mat-toolbar>
</footer> <mat-toolbar *ngIf="fastSwitching" class="fast-access">
</div> <button mat-icon-button routerLink="/service"><mat-icon>list</mat-icon></button>
<button mat-icon-button routerLink="/slides"><mat-icon>collections</mat-icon></button>
<button mat-icon-button routerLink="/alerts"><mat-icon>error</mat-icon></button>
<button mat-icon-button routerLink="/search"><mat-icon>search</mat-icon></button>
</mat-toolbar>
</footer>
</mat-sidenav-content>
</mat-sidenav-container> </mat-sidenav-container>

View File

@ -1,47 +1,36 @@
mat-sidenav { mat-sidenav {
background: white; background: white;
}
.all-wrap {
min-height: 100vh;
}
.page-wrap {
display: flex;
flex-direction: column;
min-height: 100vh;
} }
.filler { mat-sidenav-container {
flex-grow: 1; min-height: 100vh;
}
.content {
flex: 1;
} }
mat-slide-toggle { mat-slide-toggle {
margin-left: 1rem; margin-top: 0.8rem;
margin-left: 1rem;
font-size: 80%;
} }
.fast-access { .fast-access {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
} }
.footer { .footer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-evenly; justify-content: space-evenly;
} }
/* /*
* Make the Component injected by Router Outlet full height: * Make the Component injected by Router Outlet full height:
*/ */
main { main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
> *:not(router-outlet) { > *:not(router-outlet) {
flex: 1; flex: 1;
display: block; display: block;
} }
} }

View File

@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatSlideToggleChange, MatDialog } from '@angular/material';
import { State } from './responses'; import { State } from './responses';
import { OpenLPService } from './openlp.service'; import { OpenLPService } from './openlp.service';
import { MatSlideToggleChange, MatDialog } from '@angular/material'; import { PageTitleService } from './page-title.service';
import { LoginComponent } from './components/login/login.component'; import { LoginComponent } from './components/login/login.component';
@Component({ @Component({
@ -13,8 +15,11 @@ export class AppComponent implements OnInit {
fastSwitching = false; fastSwitching = false;
state = new State(); state = new State();
showLogin = false; showLogin = false;
pageTitle = 'OpenLP Remote';
constructor(private openlpService: OpenLPService, private dialog: MatDialog) { constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService,
private dialog: MatDialog) {
pageTitleService.pageTitleChanged$.subscribe(pageTitle => this.pageTitle = pageTitle);
openlpService.stateChanged$.subscribe(item => this.state = item); openlpService.stateChanged$.subscribe(item => this.state = item);
} }

View File

@ -1,5 +1,5 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule, Title } 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';
@ -18,6 +18,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { PageTitleService } from './page-title.service';
import { OpenLPService } from './openlp.service'; import { OpenLPService } from './openlp.service';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app.routing'; import { AppRoutingModule } from './app.routing';
@ -68,7 +69,9 @@ import { LoginComponent } from './components/login/login.component';
MatSnackBarModule MatSnackBarModule
], ],
providers: [ providers: [
OpenLPService PageTitleService,
OpenLPService,
Title
], ],
entryComponents: [ entryComponents: [
LoginComponent LoginComponent

View File

@ -1,9 +1,9 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { OpenLPService } from '../../openlp.service';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { PageTitleService } from '../../page-title.service';
import { OpenLPService } from '../../openlp.service';
@Component({ @Component({
selector: 'openlp-alert', selector: 'openlp-alert',
templateUrl: './alert.component.html', templateUrl: './alert.component.html',
@ -15,7 +15,10 @@ export class AlertComponent {
public alert: string; public alert: string;
constructor(private openlpService: OpenLPService, private snackBar: MatSnackBar) { } constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService,
private snackBar: MatSnackBar) {
pageTitleService.changePageTitle('Alerts');
}
onSubmit() { onSubmit() {
this.openlpService.showAlert(this.alert).subscribe(res => this.snackBar.open('Alert submitted', '', {duration: 2000})); this.openlpService.showAlert(this.alert).subscribe(res => this.snackBar.open('Alert submitted', '', {duration: 2000}));

View File

@ -1,5 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { OpenLPService } from '../../openlp.service'; import { OpenLPService } from '../../openlp.service';
import { PageTitleService } from '../../page-title.service';
import { PluginDescription } from '../../responses'; import { PluginDescription } from '../../responses';
@Component({ @Component({
@ -8,17 +10,16 @@ import { PluginDescription } from '../../responses';
styleUrls: ['./search.component.scss'], styleUrls: ['./search.component.scss'],
providers: [OpenLPService] providers: [OpenLPService]
}) })
export class SearchComponent implements OnInit { export class SearchComponent implements OnInit {
public searchPlugins: PluginDescription[] = []; public searchPlugins: PluginDescription[] = [];
public searchText = null; public searchText = null;
public searchResults = null; public searchResults = null;
public selectedPlugin = 'songs'; public selectedPlugin = 'songs';
public currentPlugin: string; public currentPlugin: string;
constructor(private openlpService: OpenLPService) {} constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService) {
pageTitleService.changePageTitle('Search');
}
onSubmit() { onSubmit() {
this.currentPlugin = this.selectedPlugin; this.currentPlugin = this.selectedPlugin;

View File

@ -1,9 +1,3 @@
<h3>Service items:</h3> <mat-card *ngFor="let item of items; let counter = index;" (click)="onItemSelected(counter)" [tabindex]="counter" class="service-item">
<div> <mat-icon>{{ getIcon(item) }}</mat-icon> {{ item.title }}
<mat-nav-list> </mat-card>
<a mat-list-item *ngFor="let item of items;let counter = index;" (click)="onItemSelected(counter)" [class.selected]="item.selected">
<mat-icon mat-list-icon>{{ getIcon(item) }}</mat-icon>
<p mat-line>{{item.title}}<p>
</a>
</mat-nav-list>
</div>

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { OpenLPService } from '../../openlp.service'; import { OpenLPService } from '../../openlp.service';
import { PageTitleService } from '../../page-title.service';
import { ServiceItem } from '../../responses'; import { ServiceItem } from '../../responses';
@Component({ @Component({
@ -13,6 +14,7 @@ import { ServiceItem } from '../../responses';
export class ServiceComponent implements OnInit { export class ServiceComponent implements OnInit {
items: ServiceItem[] = []; items: ServiceItem[] = [];
ngOnInit() { ngOnInit() {
this.getServiceItems(); this.getServiceItems();
} }
@ -26,7 +28,9 @@ export class ServiceComponent implements OnInit {
this.openlpService.getServiceItems().subscribe(items => this.items = items); this.openlpService.getServiceItems().subscribe(items => this.items = items);
} }
constructor(private openlpService: OpenLPService, private router: Router) { constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService,
private router: Router) {
pageTitleService.changePageTitle('Service');
openlpService.stateChanged$.subscribe(item => this.getServiceItems()); openlpService.stateChanged$.subscribe(item => this.getServiceItems());
} }

View File

@ -1,9 +1,4 @@
<h3>Slides:</h3> <mat-card mat-list-item *ngFor="let slide of slides; let counter = index;" (click)="onSlideSelected(counter)" [class.selected]="slide.selected">
<div> <div class="verse-tag">{{ slide.tag }}</div>
<mat-nav-list> <div class="verse-text">{{ slide.text }}</div>
<mat-list-item *ngFor="let slide of slides; let counter = index;" (click)="onSlideSelected(counter)" [class.selected]="slide.selected"> </mat-card>
<span MatListAvatar>{{ slide.tag }}</span>
<span MatLine style="margin-left: 2rem;">{{ slide.text }}</span>
</mat-list-item>
</mat-nav-list>
</div>

View File

@ -1,3 +1,16 @@
mat-card {
cursor: pointer;
}
.selected { .selected {
font-weight: 700; font-weight: 700;
} }
.verse-tag {
float: left;
}
.verse-text {
margin-left: 2.5rem;
white-space: pre-wrap;
}

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { OpenLPService } from '../../openlp.service'; import { OpenLPService } from '../../openlp.service';
import { PageTitleService } from '../../page-title.service';
@Component({ @Component({
selector: 'openlp-slides', selector: 'openlp-slides',
@ -12,6 +12,12 @@ import { OpenLPService } from '../../openlp.service';
export class SlidesComponent implements OnInit { export class SlidesComponent implements OnInit {
slides = null; slides = null;
constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService) {
pageTitleService.changePageTitle('Slides');
openlpService.stateChanged$.subscribe(item => this.getSlides());
}
ngOnInit() { ngOnInit() {
this.getSlides(); this.getSlides();
} }
@ -23,8 +29,4 @@ export class SlidesComponent implements OnInit {
getSlides() { getSlides() {
this.openlpService.getItemSlides().subscribe(slides => this.slides = slides); this.openlpService.getItemSlides().subscribe(slides => this.slides = slides);
} }
constructor(private openlpService: OpenLPService) {
openlpService.stateChanged$.subscribe(item => this.getSlides());
}
} }

View File

@ -1,11 +1,20 @@
import { Injectable, EventEmitter } from '@angular/core'; import { Injectable, EventEmitter } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PluginDescription, State, Slide, ServiceItem, MainView, SystemInformation, Credentials, AuthToken } 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) => {
const inst = new cls(); const inst = new cls();
for (const p in json) { for (const p in json) {
@ -21,12 +30,14 @@ const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'}) headers: new HttpHeaders({'Content-Type': 'application/json'})
}; };
@Injectable() @Injectable()
export class OpenLPService { export class OpenLPService {
private apiURL: string; private apiURL: string;
public stateChanged$: EventEmitter<State>; public stateChanged$: EventEmitter<State>;
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
const host = window.location.hostname;
let port: string; let port: string;
if (environment.production) { if (environment.production) {
port = window.location.port; port = window.location.port;
@ -34,12 +45,11 @@ export class OpenLPService {
else { else {
port = '4316'; port = '4316';
} }
this.apiURL = `http://localhost:${port}/api/v1`; this.apiURL = `http://${host}:${port}/api/v1`;
this.stateChanged$ = new EventEmitter<State>(); this.stateChanged$ = new EventEmitter<State>();
this.retrieveSystemInformation().subscribe(info => { this.retrieveSystemInformation().subscribe(info => {
const ws = new WebSocket(`ws://localhost:${info.websocket_port}/state`); const ws = new WebSocket(`ws://${host}:${info.websocket_port}/state`);
ws.onmessage = (event) => { ws.onmessage = (event) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {

View File

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Subject } from 'rxjs';
@Injectable()
export class PageTitleService {
private pageTitleSource = new Subject<string>();
public pageTitleChanged$ = this.pageTitleSource.asObservable();
constructor(private titleService: Title) {}
changePageTitle(pageTitle: string) {
this.pageTitleSource.next(pageTitle);
this.titleService.setTitle(pageTitle + ' | OpenLP Remote');
}
}

View File

@ -2,12 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>OpenLP &bull; Remote</title> <title>OpenLP Remote</title>
<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="/assets/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>

View File

@ -1,25 +1,60 @@
/* 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/theming';
@include mat-core();
$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-light-blue, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);
* { * {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
}
body {
font: 500 20px/32px Roboto, "Helvetica Neue", sans-serif;
}
html, body {
margin: 0;
padding: 0;
} }
mat-sidenav-layout { mat-sidenav-layout {
background: rgba(0,0,0,0.03); background: rgba(0,0,0,0.03);
} }
mat-sidenav { mat-sidenav {
width: 200px; width: 12rem;
}
mat-card {
margin-bottom: 1rem;
}
mat-card.service-item,
mat-card.slide {
cursor: pointer;
} }
.displayButton .active { .displayButton .active {
background: 'teal'; background: 'teal';
}
.page-title {
margin-left: 1rem;
} }
.content { .content {
margin: 20px; margin: 1rem;
}
footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
} }
.chordline { .chordline {

View File

@ -72,7 +72,6 @@
"no-trailing-whitespace": true, "no-trailing-whitespace": true,
"no-unnecessary-initializer": true, "no-unnecessary-initializer": true,
"no-unused-expression": true, "no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true, "no-var-keyword": true,
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"one-line": [ "one-line": [
@ -115,12 +114,12 @@
"check-type" "check-type"
], ],
"no-output-on-prefix": true, "no-output-on-prefix": true,
"use-input-property-decorator": true, "no-inputs-metadata-property": true,
"use-output-property-decorator": true, "no-outputs-metadata-property": true,
"use-host-property-decorator": true, "no-host-metadata-property": true,
"no-input-rename": true, "no-input-rename": true,
"no-output-rename": true, "no-output-rename": true,
"use-life-cycle-interface": true, "use-lifecycle-interface": true,
"use-pipe-transform-interface": true, "use-pipe-transform-interface": true,
"component-class-suffix": true, "component-class-suffix": true,
"directive-class-suffix": true "directive-class-suffix": true