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 @@
-
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;
+}