diff --git a/angular.json b/angular.json index bf9160f..8b4a726 100644 --- a/angular.json +++ b/angular.json @@ -48,6 +48,14 @@ "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true } } }, @@ -59,8 +67,12 @@ "configurations": { "production": { "browserTarget": "@openlp/web-remote:build:production" + }, + "development": { + "browserTarget": "@openlp/web-remote:build:development" } - } + }, + "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", diff --git a/src/app/app.component.html b/src/app/app.component.html index 648b30a..42901fe 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -28,7 +28,7 @@ Stage View Chord View - Fast switching + settings Settings diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2b02e7a..9039bdf 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,6 +12,7 @@ import { fromEvent } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { DisplayModeSelectorComponent } from './components/display-mode-selector/display-mode-selector.component'; import { HotKeysService } from './hotkeys.service'; +import { SettingsService } from './settings.service'; // import { version } from '../../package.json'; @Component({ @@ -23,16 +24,16 @@ export class AppComponent implements OnInit { // Make DisplayMode enum visible to html code DisplayMode = DisplayMode; - private _fastSwitching = false; state = new State(); showLogin = false; pageTitle = 'OpenLP Remote'; appVersion = '0.0'; webSocketOpen = false; + fastSwitching = false; constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService, private dialog: MatDialog, private bottomSheet: MatBottomSheet, private windowRef: WindowRef, - private hotKeysService: HotKeysService) { + private hotKeysService: HotKeysService, private settingsService: SettingsService) { pageTitleService.pageTitleChanged$.subscribe(pageTitle => this.pageTitle = pageTitle); openlpService.stateChanged$.subscribe(item => this.state = item); openlpService.webSocketStateChanged$.subscribe(status => this.webSocketOpen = status === WebSocketStatus.Open); @@ -48,6 +49,8 @@ export class AppComponent implements OnInit { ngOnInit(): void { this.openlpService.retrieveSystemInformation().subscribe(res => this.showLogin = res.login_required); this.addHotKeys(); + this.fastSwitching = this.settingsService.get('fastSwitching'); + this.settingsService.onPropertyChanged('fastSwitching').subscribe(value => this.fastSwitching = value); } addHotKeys(): void { @@ -87,18 +90,6 @@ export class AppComponent implements OnInit { ); } - get fastSwitching(): boolean { - if (localStorage.getItem('OpenLP-fastSwitching')) { - this._fastSwitching = JSON.parse(localStorage.getItem('OpenLP-fastSwitching')); - } - return this._fastSwitching; - } - - set fastSwitching(value: boolean) { - this._fastSwitching = value; - localStorage.setItem('OpenLP-fastSwitching', JSON.stringify(value)); - } - openDisplayModeSelector(): void { const selectorRef = this.bottomSheet.open(DisplayModeSelectorComponent, {data: this.state.displayMode}); selectorRef.afterDismissed().subscribe(result => { @@ -154,10 +145,6 @@ export class AppComponent implements OnInit { this.openlpService.showDisplay().subscribe(); } - sliderChanged(event: MatSlideToggleChange) { - this.fastSwitching = event.checked; - } - forceWebSocketReconnection() { this.openlpService.reconnectWebSocketIfNeeded(); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cccedd0..8b34804 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -21,6 +21,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; +import { MatSliderModule } from '@angular/material/slider'; import { AppComponent } from './app.component'; import { PageTitleService } from './page-title.service'; @@ -46,6 +47,8 @@ import { ServiceListComponent } from './components/service/service-list/service- import { ChordViewItemComponent } from './components/chord-view/chord-view-item/chord-view-item.component'; import { StageViewItemComponent } from './components/stage-view/stage-view-item/stage-view-item.component'; import { DisplayModeSelectorComponent } from './components/display-mode-selector/display-mode-selector.component'; +import { SettingsComponent } from './components/settings/settings.component'; +import { StageChordPreviewComponent } from './components/settings/stage-chord-preview/stage-chord-preview.component'; @NgModule({ @@ -69,7 +72,9 @@ import { DisplayModeSelectorComponent } from './components/display-mode-selector SlideListComponent, SlideItemComponent, ThemesComponent, - DisplayModeSelectorComponent + DisplayModeSelectorComponent, + SettingsComponent, + StageChordPreviewComponent ], imports: [ BrowserModule, @@ -93,7 +98,8 @@ import { DisplayModeSelectorComponent } from './components/display-mode-selector MatTabsModule, MatToolbarModule, MatTooltipModule, - MatBottomSheetModule + MatBottomSheetModule, + MatSliderModule ], providers: [ PageTitleService, diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 11c90ac..a9ab0c7 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -9,6 +9,7 @@ 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'; import { ThemesComponent } from './components/themes/themes.component'; +import { SettingsComponent } from './components/settings/settings.component'; const routes: Routes = [ { path: '', redirectTo: '/service', pathMatch: 'full' }, @@ -19,7 +20,8 @@ const routes: Routes = [ { path: 'chords', component: ChordViewComponent }, { path: 'main', component: MainViewComponent }, { path: 'stage', component: StageViewComponent }, - { path: 'themes', component: ThemesComponent} + { path: 'themes', component: ThemesComponent}, + { path: 'settings', component: SettingsComponent} ]; @NgModule({ imports: [RouterModule.forRoot(routes)], diff --git a/src/app/components/_overlay-common.scss b/src/app/components/_overlay-common.scss index dfff3cd..4d496db 100644 --- a/src/app/components/_overlay-common.scss +++ b/src/app/components/_overlay-common.scss @@ -1,18 +1,19 @@ $mobile-breakpoint: 1024px; -@mixin slide-font-size($scale: 1, $desktop-scale: $scale) { - font-size: calc(#{4vw * $scale} + #{1.5vh * $scale}); +@mixin slide-font-size($scale: 1, $desktop-scale: $scale, $stage-var-name: stage) { + $var-sentence: var(--openlp-#{$stage-var-name}-font-scale); + font-size: calc((#{4vw * $scale} * #{$var-sentence}) + (#{1.5vh * $scale} * #{$var-sentence})); @media (orientation: landscape) { - font-size: #{6vmin * $scale}; + font-size: calc(#{6vmin * $scale} * #{$var-sentence}); } @media (orientation: landscape) and (max-aspect-ratio: 16/9) { - font-size: #{3vw * $scale}; + font-size: calc(#{3vw * $scale} * #{$var-sentence}); } @media screen and (min-width: $mobile-breakpoint) { - font-size: calc(#{3.1vw * $desktop-scale} + #{1.5vh * $desktop-scale}); + font-size: calc((#{3.1vw * $desktop-scale} * #{$var-sentence}) + (#{1.5vh * $desktop-scale} * #{$var-sentence})); //font-size: #{4vw * $scale}; //font-size: #{5.6vmin * $scale}; } diff --git a/src/app/components/chord-view/chord-view.component.html b/src/app/components/chord-view/chord-view.component.html index eae9fd1..e653a25 100644 --- a/src/app/components/chord-view/chord-view.component.html +++ b/src/app/components/chord-view/chord-view.component.html @@ -1,4 +1,8 @@ -
+
{{ tag.text }} @@ -23,7 +27,7 @@
- + arrow_back
diff --git a/src/app/components/chord-view/chord-view.component.ts b/src/app/components/chord-view/chord-view.component.ts index b3a253a..09ee95b 100644 --- a/src/app/components/chord-view/chord-view.component.ts +++ b/src/app/components/chord-view/chord-view.component.ts @@ -1,7 +1,5 @@ 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({ @@ -16,6 +14,7 @@ export class ChordViewComponent extends StageViewComponent { songTransposeMap = new Map(); // current songs transpose level transposeLevel = 0; + stageProperty = 'chords'; setNewSlides(slides: Slide[]): void { diff --git a/src/app/components/overlay.scss b/src/app/components/overlay.scss index f71bc08..9d4fc62 100644 --- a/src/app/components/overlay.scss +++ b/src/app/components/overlay.scss @@ -1,28 +1,35 @@ @import "./overlay-common"; +:root { + --openlp-stage-font-scale: 1; + --openlp-stage-image-scale: 1; +} + .overlay { background: black; width: 100%; height: 100%; - position: fixed; - left: 0; - top: 0; - z-index: 1200; overflow: hidden; color: white; display: flex; justify-content: flex-start; flex-direction: row; + &:not(.embedded) { + position: fixed; + left: 0; + top: 0; + z-index: 1200; + } .active-slide-img { /* properly size current slide thumbnail */ /* relative sizes might not work in real world */ /* If we get larger thumbnail sizes, we want to limit their size */ - max-height: 75%; - min-height: 250px; + max-height: calc(75% * var(--openlp-stage-image-scale)); + min-height: calc(250px * var(--openlp-stage-image-scale)); } .active-slide-img-text { - font-size: 1.8rem; + @include slide-font-size(0.75); } .next-slides-img { /* properly size thumbnail displayed in 2nd and subsequent slides */ @@ -32,7 +39,7 @@ } .next-slides-text { - font-size: 1.4rem; + @include slide-font-size(0.5); } &-content { diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html new file mode 100644 index 0000000..8e0f792 --- /dev/null +++ b/src/app/components/settings/settings.component.html @@ -0,0 +1,59 @@ +
+ + + User Interface + + +
+ + Enable Fast Switching panel + +
+
+
+ + + Stage and Chords Appearance + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + +
+
+
\ No newline at end of file diff --git a/src/app/components/settings/settings.component.scss b/src/app/components/settings/settings.component.scss new file mode 100644 index 0000000..6b6afd7 --- /dev/null +++ b/src/app/components/settings/settings.component.scss @@ -0,0 +1,26 @@ +.settings-panel { + max-width: 600px; + margin: 0 auto; + + @media screen and (min-height: 836px) { + max-width: 768px; + } +} + +.settings-item { + font-size: 1rem; + + &:first-child { + margin-top: 1em; + } + + label { + width: 100%; + display: block; + margin-bottom: -1em; + } + mat-slider { + width: calc(100% - 3rem); + margin: 0 auto; + } +} diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts new file mode 100644 index 0000000..03f42e9 --- /dev/null +++ b/src/app/components/settings/settings.component.ts @@ -0,0 +1,40 @@ +import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { OpenLPService } from '../../openlp.service'; +import { PageTitleService } from '../../page-title.service'; +import { SettingsProperties, SettingsPropertiesItem, SettingsService } from '../../settings.service'; + +@Component({ + selector: 'openlp-settings', + templateUrl: `./settings.component.html`, + styleUrls: [`./settings.component.scss`] +}) +export class SettingsComponent implements OnDestroy { + constructor( + protected pageTitleService: PageTitleService, + protected openlpService: OpenLPService, + protected settingsService: SettingsService, + ) { + this.settingsSubscription$ = settingsService.settingChanged$.subscribe(this._settingChanged); + pageTitleService.changePageTitle('Settings'); + } + + protected settingsSubscription$: Subscription; + + settings: Partial = this.settingsService.getAll(); + + setSetting(property: SP, value: SV) { + this.settingsService.set(property, value); + } + + _settingChanged = ( + value: SettingsPropertiesItem + ) => { + this.settings = {...this.settings, [value.property]: value.value}; + }; + + ngOnDestroy(): void { + this.settingsSubscription$.unsubscribe(); + } + +} diff --git a/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.html b/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.html new file mode 100644 index 0000000..e136efc --- /dev/null +++ b/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.html @@ -0,0 +1,14 @@ +
+ + +
\ No newline at end of file diff --git a/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.scss b/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.scss new file mode 100644 index 0000000..134e1bf --- /dev/null +++ b/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.scss @@ -0,0 +1,14 @@ +.stage-preview-container { + position: relative; + + > * { + pointer-events: none; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + border: none; + transform-origin: 0 0; + } +} \ No newline at end of file diff --git a/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.ts b/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.ts new file mode 100644 index 0000000..d5432e7 --- /dev/null +++ b/src/app/components/settings/stage-chord-preview/stage-chord-preview.component.ts @@ -0,0 +1,107 @@ +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild +} from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { SettingsProperties, SettingsService } from 'src/app/settings.service'; + +@Component({ + selector: 'openlp-stage-chord-preview', + templateUrl: './stage-chord-preview.component.html', + styleUrls: ['./stage-chord-preview.component.scss'], +}) +export class StageChordPreviewComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { + constructor( + protected settingsService: SettingsService, + protected ref: ChangeDetectorRef + ) { + this.windowResizeSubscription$ = fromEvent(window, 'resize') + .pipe(debounceTime(300)) + .subscribe(() => this._resizeElement()); + } + + @Input() stageType: 'stage' | 'chords' = 'stage'; + @ViewChild('stageView', {read: ElementRef}) stageView: ElementRef; + @ViewChild('chordsView', {read: ElementRef}) chordsView: ElementRef; + @ViewChild('stageViewContainer') stageViewContainer: ElementRef; + fontScale: number; + protected windowResizeSubscription$: Subscription; + protected settingChangedSubscription$: Subscription; + + ngOnInit(): void { + this.fontScale = this.settingsService.get( + this.stageType + 'FontScale' as keyof SettingsProperties + ) as number / 100; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['stageType'].currentValue !== changes['stageType'].previousValue) { + this.settingChangedSubscription$?.unsubscribe(); + this.settingChangedSubscription$ = this.settingsService + .onPropertyChanged(changes['stageType'].currentValue + 'FontScale' as keyof SettingsProperties) + .subscribe(value => { + this.fontScale = value as number / 100; + this.ref.detectChanges(); + }); + } + } + + ngAfterViewInit(): void { + if (this._getStageViewElement()?.nativeElement) { + this._resizeElement(); + } + } + + ngOnDestroy(): void { + this.windowResizeSubscription$?.unsubscribe(); + this.settingChangedSubscription$?.unsubscribe(); + } + + _getStageViewElement() { + switch (this.stageType) { + case 'stage': + return this.stageView; + case 'chords': + return this.chordsView; + } + } + + _resizeElement() { + const viewElement = this._getStageViewElement(); + if (this.stageViewContainer?.nativeElement && viewElement?.nativeElement) { + // Resetting container to 100% width before calculating + this.stageViewContainer.nativeElement.style.width = '100%'; + this.stageViewContainer.nativeElement.style.height = 'auto'; + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + const {width: containerWidth} = this.stageViewContainer?.nativeElement.getBoundingClientRect(); + + let zoomScale = containerWidth / windowWidth; + const targetContainerHeight = containerWidth * (windowHeight / windowWidth); + let scaleTranslate = ''; + + if (targetContainerHeight > (windowHeight * 0.6)) { + zoomScale *= 0.5; + scaleTranslate = ' translateX(50%)'; + } + + // Setting the container width + height to scale after + this.stageViewContainer.nativeElement.style.width = (windowWidth * zoomScale) + 'px'; + this.stageViewContainer.nativeElement.style.height = (windowHeight * zoomScale) + 'px'; + + viewElement.nativeElement.style.width = windowWidth + 'px'; + viewElement.nativeElement.style.height = windowHeight + 'px'; + viewElement.nativeElement.style.transform = `scale(${zoomScale})${scaleTranslate}`; + } + } +} diff --git a/src/app/components/stage-view/stage-view.component.html b/src/app/components/stage-view/stage-view.component.html index 0567c3b..4c4786c 100644 --- a/src/app/components/stage-view/stage-view.component.html +++ b/src/app/components/stage-view/stage-view.component.html @@ -1,4 +1,8 @@ -
+
{{ tag.text }} @@ -11,7 +15,7 @@
- + arrow_back
- diff --git a/src/app/components/stage-view/stage-view.component.ts b/src/app/components/stage-view/stage-view.component.ts index bf35ad4..d689c1a 100644 --- a/src/app/components/stage-view/stage-view.component.ts +++ b/src/app/components/stage-view/stage-view.component.ts @@ -1,4 +1,7 @@ -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { SettingsProperties, SettingsService } from 'src/app/settings.service'; import { OpenLPService } from '../../openlp.service'; import { ServiceItem, Slide } from '../../responses'; @@ -13,7 +16,8 @@ interface Tag { styleUrls: ['./stage-view.component.scss', '../overlay.scss'], encapsulation: ViewEncapsulation.None }) -export class StageViewComponent implements OnInit { +export class StageViewComponent implements OnInit, OnDestroy { + @Input() embedded = false; serviceItem: ServiceItem = null; notes = ''; currentSlides: Slide[] = []; @@ -21,14 +25,38 @@ export class StageViewComponent implements OnInit { tags: Tag[] = []; time = new Date(); showNotes = true; + fontScale: number; - constructor(public openlpService: OpenLPService) { + fontScaleSubscription$: Subscription; + + stageProperty = 'stage'; + + constructor( + public openlpService: OpenLPService, + protected route: ActivatedRoute, + protected settingsService: SettingsService, + protected ref: ChangeDetectorRef + ) { setInterval(() => this.time = new Date(), 1000); } ngOnInit() { this.updateCurrentSlides(); this.openlpService.stateChanged$.subscribe(item => this.updateCurrentSlides()); + this.fontScale = this.settingsService.get( + this.stageProperty + 'FontScale' as keyof SettingsProperties + ) as number / 100; + + this.fontScaleSubscription$ = this.settingsService + .onPropertyChanged(this.stageProperty + 'FontScale' as keyof SettingsProperties) + .subscribe(value => { + this.fontScale = value as number / 100; + this.ref.detectChanges(); + }); + } + + ngOnDestroy(): void { + this.fontScaleSubscription$?.unsubscribe(); } updateCurrentSlides(): void { diff --git a/src/app/settings.service.ts b/src/app/settings.service.ts new file mode 100644 index 0000000..8c5e1fa --- /dev/null +++ b/src/app/settings.service.ts @@ -0,0 +1,89 @@ +import { EventEmitter, Injectable } from '@angular/core'; + +// Set here the default value; if there's none, set as undefined and specify key type. +export class SettingsProperties { + fastSwitching = false; + stageFontScale = 100; + chordsFontScale = 100; +}; + +export interface SettingsPropertiesItem { + property: SP; + value: SV; +} + +const LOCAL_STORAGE_PREFIX = 'OpenLP-'; + +@Injectable({providedIn: 'root'}) +export class SettingsService { + constructor() { + window.addEventListener('storage', this._handleStorageEvent); + } + + defaultSettingsPropertiesInstance = new SettingsProperties(); + + settingChanged$: EventEmitter> = new EventEmitter(); + listenersCache: {[key in keyof Partial]: EventEmitter} = {}; + + getAll(): Partial { + const output: Partial = {}; + for (const key of Object.keys(this.defaultSettingsPropertiesInstance)) { + const value = this.get(key as keyof SettingsProperties); + if (value !== undefined) { + output[key] = value; + } + } + return output; + } + + get(property: SP): SV | undefined { + let propertyValue: any = localStorage.getItem(LOCAL_STORAGE_PREFIX + property); + if ((propertyValue === undefined || propertyValue === null) + && this.defaultSettingsPropertiesInstance.hasOwnProperty(property) + ) { + propertyValue = this.defaultSettingsPropertiesInstance[property]; + this.set(property, propertyValue); + } + + if (propertyValue) { + return JSON.parse(propertyValue); + } + + return undefined; + } + + set(property: SP, value: SV) { + if (value === undefined) { + localStorage.removeItem(LOCAL_STORAGE_PREFIX + property); + this._emitEvent(property, undefined); + } else { + localStorage.setItem(LOCAL_STORAGE_PREFIX + property, JSON.stringify(value)); + this._emitEvent(property, value); + } + } + + remove(property: SP) { + this.set(property, undefined); + } + + onPropertyChanged( + property: SP + ): EventEmitter { + if (!this.listenersCache[property]) { + this.listenersCache[property] = new EventEmitter(); + } + + return this.listenersCache[property]; + } + + protected _handleStorageEvent = (event: StorageEvent) => { + if (event.storageArea === localStorage) { + this._emitEvent(event.key.replace(LOCAL_STORAGE_PREFIX, '') as any, JSON.parse(event.newValue)); + } + }; + + protected _emitEvent(property: SP, value: SV) { + this.settingChanged$.emit({property, value}); + this.listenersCache?.[property]?.emit(value); + } +} diff --git a/src/styles.scss b/src/styles.scss index 780c15b..43423ea 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -107,3 +107,7 @@ footer { .mat-mdc-tooltip { font-size: 1rem; } + +footer { + z-index: 1; +}