Merge branch 'use-configured-shortcuts' into 'master'

Make use of the configured shortcuts in OpenLP.

See merge request openlp/web-remote!83
This commit is contained in:
Raoul Snyman 2024-04-02 04:54:12 +00:00
commit 01724bb136
8 changed files with 635 additions and 496 deletions

View File

@ -24,44 +24,44 @@
"supportedBrowsers": "(echo module.exports = && browserslist-useragent-regexp --allowHigherVersions) > src/assets/supportedBrowsers.js" "supportedBrowsers": "(echo module.exports = && browserslist-useragent-regexp --allowHigherVersions) > src/assets/supportedBrowsers.js"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^17.3.1", "@angular/animations": "^17.3.2",
"@angular/cdk": "^17.3.1", "@angular/cdk": "^17.3.2",
"@angular/common": "^17.3.1", "@angular/common": "^17.3.2",
"@angular/compiler": "^17.3.1", "@angular/compiler": "^17.3.2",
"@angular/core": "^17.3.1", "@angular/core": "^17.3.2",
"@angular/forms": "^17.3.1", "@angular/forms": "^17.3.2",
"@angular/material": "^17.3.1", "@angular/material": "^17.3.2",
"@angular/platform-browser": "^17.3.1", "@angular/platform-browser": "^17.3.2",
"@angular/platform-browser-dynamic": "^17.3.1", "@angular/platform-browser-dynamic": "^17.3.2",
"@angular/router": "^17.3.1", "@angular/router": "^17.3.2",
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.12",
"core-js": "^3.35.1", "core-js": "^3.36.1",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"material-icons": "^1.13.12", "material-icons": "^1.13.12",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"zone.js": "^0.14.3" "zone.js": "^0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.3.1", "@angular-devkit/build-angular": "^17.3.2",
"@angular-eslint/builder": "^17.3.0", "@angular-eslint/builder": "^17.3.0",
"@angular-eslint/eslint-plugin": "^17.3.0", "@angular-eslint/eslint-plugin": "^17.3.0",
"@angular-eslint/eslint-plugin-template": "^17.3.0", "@angular-eslint/eslint-plugin-template": "^17.3.0",
"@angular-eslint/schematics": "^17.3.0", "@angular-eslint/schematics": "^17.3.0",
"@angular-eslint/template-parser": "^17.3.0", "@angular-eslint/template-parser": "^17.3.0",
"@angular/cli": "~17.3.1", "@angular/cli": "~17.3.2",
"@angular/compiler-cli": "^17.3.1", "@angular/compiler-cli": "^17.3.2",
"@angular/language-service": "^17.3.1", "@angular/language-service": "^17.3.2",
"@chiragrupani/karma-chromium-edge-launcher": "^2.3.1", "@chiragrupani/karma-chromium-edge-launcher": "^2.3.1",
"@types/jasmine": "~5.1.4", "@types/jasmine": "~5.1.4",
"@types/jasminewd2": "~2.0.13", "@types/jasminewd2": "~2.0.13",
"@types/node": "~20.11.17", "@types/node": "~20.12.2",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "7.4.0",
"@typescript-eslint/parser": "6.21.0", "@typescript-eslint/parser": "7.4.0",
"browserslist": "^4.23.0", "browserslist": "^4.23.0",
"browserslist-useragent-regexp": "^4.1.1", "browserslist-useragent-regexp": "^4.1.1",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-plugin-import": "~2.29.1", "eslint-plugin-import": "~2.29.1",
"eslint-plugin-jsdoc": "~48.0.6", "eslint-plugin-jsdoc": "~48.2.2",
"eslint-plugin-prefer-arrow": "~1.2.3", "eslint-plugin-prefer-arrow": "~1.2.3",
"jasmine-core": "~5.1.2", "jasmine-core": "~5.1.2",
"jasmine-spec-reporter": "~7.0.0", "jasmine-spec-reporter": "~7.0.0",
@ -71,7 +71,7 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0", "karma-jasmine-html-reporter": "^2.1.0",
"ts-node": "~10.9.2", "ts-node": "~10.9.2",
"typescript": "~5.3.3" "typescript": "~5.4.3"
}, },
"private": true "private": true
} }

View File

@ -10,7 +10,8 @@ import { LoginComponent } from './components/login/login.component';
import { fromEvent } from 'rxjs'; import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators'; import { debounceTime } from 'rxjs/operators';
import { DisplayModeSelectorComponent } from './components/display-mode-selector/display-mode-selector.component'; import { DisplayModeSelectorComponent } from './components/display-mode-selector/display-mode-selector.component';
import { HotKeysService } from './hotkeys.service'; import { Shortcuts, ShortcutsService } from './shortcuts.service';
import { ShortcutPipe } from './components/shortcuts/shortcut.pipe';
import { SettingsService } from './settings.service'; import { SettingsService } from './settings.service';
import * as supportedBrowsers from '../assets/supportedBrowsers'; import * as supportedBrowsers from '../assets/supportedBrowsers';
@ -29,14 +30,16 @@ export class AppComponent implements OnInit {
appVersion = '0.0'; appVersion = '0.0';
webSocketOpen = false; webSocketOpen = false;
fastSwitching = false; fastSwitching = false;
useShortcutsFromOpenlp = false;
constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService, constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService,
private dialog: MatDialog, private bottomSheet: MatBottomSheet, private windowRef: WindowRef, private dialog: MatDialog, private bottomSheet: MatBottomSheet, private windowRef: WindowRef,
private hotKeysService: HotKeysService, private settingsService: SettingsService) { private shortcutsService: ShortcutsService, private settingsService: SettingsService) {
pageTitleService.pageTitleChanged$.subscribe(pageTitle => this.pageTitle = pageTitle); this.pageTitleService.pageTitleChanged$.subscribe(pageTitle => this.pageTitle = pageTitle);
openlpService.stateChanged$.subscribe(item => this.state = item); this.openlpService.stateChanged$.subscribe(item => this.state = item);
openlpService.webSocketStateChanged$.subscribe(status => this.webSocketOpen = status === WebSocketStatus.Open); this.openlpService.webSocketStateChanged$.subscribe(status => this.webSocketOpen = status === WebSocketStatus.Open);
this.appVersion = windowRef.nativeWindow.appVersion || '0.0'; this.shortcutsService.shortcutsChanged$.subscribe(shortcuts => this.addShortcuts(shortcuts));
this.appVersion = this.windowRef.nativeWindow.appVersion || '0.0';
this.webSocketOpen = openlpService.webSocketStatus === WebSocketStatus.Open; this.webSocketOpen = openlpService.webSocketStatus === WebSocketStatus.Open;
// Try to force websocket reconnection as user is now focused on window and will try to interact soon // Try to force websocket reconnection as user is now focused on window and will try to interact soon
// Adding a debounce to avoid event flooding // Adding a debounce to avoid event flooding
@ -49,47 +52,60 @@ export class AppComponent implements OnInit {
if (!(supportedBrowsers.test(navigator.userAgent))) { if (!(supportedBrowsers.test(navigator.userAgent))) {
window.location.replace("/assets/notsupported.html"); window.location.replace("/assets/notsupported.html");
} }
this.openlpService.retrieveSystemInformation().subscribe(res => this.showLogin = res.login_required); this.openlpService.retrieveSystemInformation().subscribe(res => {
this.addHotKeys(); this.showLogin = res.login_required
this.useShortcutsFromOpenlp = this.openlpService.assertApiVersionMinimum(2, 5)
this.shortcutsService.getShortcuts(this.useShortcutsFromOpenlp);
}
);
this.fastSwitching = this.settingsService.get('fastSwitching'); this.fastSwitching = this.settingsService.get('fastSwitching');
this.settingsService.onPropertyChanged('fastSwitching').subscribe(value => this.fastSwitching = value); this.settingsService.onPropertyChanged('fastSwitching').subscribe(value => this.fastSwitching = value);
} }
addHotKeys(): void { addShortcuts(shortcuts: Shortcuts): void {
this.hotKeysService.addShortcut({ keys: 'ArrowUp' }).subscribe(() => const shortcutPipe = new ShortcutPipe();
this.previousSlide() shortcuts.previousSlide.forEach((key) => {
); this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.hotKeysService.addShortcut({ keys: 'ArrowDown' }).subscribe(() => this.previousSlide()
this.nextSlide() )
); });
this.hotKeysService.addShortcut({ keys: 'PageUp' }).subscribe(() => shortcuts.nextSlide.forEach((key) => {
this.previousSlide() this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
); this.nextSlide()
this.hotKeysService.addShortcut({ keys: 'PageDown' }).subscribe(() => )
this.nextSlide() });
); shortcuts.previousItem.forEach((key) => {
this.hotKeysService.addShortcut({ keys: 'ArrowLeft' }).subscribe(() => this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.previousItem() this.previousItem()
); )
this.hotKeysService.addShortcut({ keys: 'ArrowRight' }).subscribe(() => });
this.nextItem() shortcuts.nextItem.forEach((key) => {
); this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.hotKeysService.addShortcut({ keys: 'Space' }).subscribe(() => this.nextItem()
{ )
});
shortcuts.showDisplay.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() => {
if (this.state.displayMode !== DisplayMode.Presentation) { if (this.state.displayMode !== DisplayMode.Presentation) {
this.showDisplay(); this.showDisplay();
} }
} })
); });
this.hotKeysService.addShortcut({ keys: 't' }).subscribe(() => shortcuts.themeDisplay.forEach((key) => {
this.state.displayMode === DisplayMode.Theme ? this.showDisplay() : this.themeDisplay() this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
); this.state.displayMode === DisplayMode.Theme ? this.showDisplay() : this.themeDisplay()
this.hotKeysService.addShortcut({ keys: 'code.Period' }).subscribe(() => )
this.state.displayMode === DisplayMode.Blank ? this.showDisplay() : this.blankDisplay() });
); shortcuts.blankDisplay.forEach((key) => {
this.hotKeysService.addShortcut({ keys: 'd' }).subscribe(() => this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.state.displayMode === DisplayMode.Desktop ? this.showDisplay() : this.desktopDisplay() this.state.displayMode === DisplayMode.Blank ? this.showDisplay() : this.blankDisplay()
); )
});
shortcuts.desktopDisplay.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.state.displayMode === DisplayMode.Desktop ? this.showDisplay() : this.desktopDisplay()
)
});
} }
openDisplayModeSelector(): void { openDisplayModeSelector(): void {

View File

@ -0,0 +1,23 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'shortcut'})
export class ShortcutPipe implements PipeTransform {
transform(value: string): string {
if (!value) {
return value;
}
if (typeof value !== 'string') {
throw Error(`Invalid pipe argument: '${value}' for pipe 'ShortcutPipe'`);
}
value = value.replace('.', 'code.period');
value = value.replace('PgUp', 'pageup');
value = value.replace('PgDown', 'pagedown');
value = value.replace('Up', 'arrowup');
value = value.replace('Down', 'arrowdown');
value = value.replace('Left', 'arrowleft');
value = value.replace('Right', 'arrowright');
value = value.replace(/(Alt|Shift)\+/, '$1.');
value = value.replace(/Ctrl\+/, 'control.');
return value.toLowerCase();
}
}

View File

@ -1,45 +0,0 @@
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { Observable } from 'rxjs';
interface Options {
element: any;
keys: string;
}
@Injectable({ providedIn: 'root' })
export class HotKeysService {
defaults: Partial<Options> = {
element: this.document
};
constructor(private eventManager: EventManager, @Inject(DOCUMENT) private document: Document) {
}
addShortcut(options: Partial<Options>) {
const merged = { ...this.defaults, ...options };
const event = `keydown.${merged.keys}`;
return new Observable(observer => {
const handler = (e: KeyboardEvent) => {
const activeElement = this.document.activeElement;
const notOnInput = activeElement?.tagName !== 'INPUT' && activeElement?.tagName !== 'TEXTAREA';
if (notOnInput) {
if (document.URL.endsWith('/slides')) {
e.preventDefault();
observer.next(e);
}
}
};
const dispose = this.eventManager.addEventListener(
merged.element, event, handler
);
return () => {
dispose();
};
});
}
}

View File

@ -10,6 +10,7 @@ import {
ServiceItem, ServiceItem,
Theme, Theme,
MainView, MainView,
Shortcut,
SystemInformation, SystemInformation,
Credentials, Credentials,
AuthToken, AuthToken,
@ -96,6 +97,10 @@ export class OpenLPService {
return this.doGet<MainView>(`${this.apiURL}/core/live-image`); return this.doGet<MainView>(`${this.apiURL}/core/live-image`);
} }
getShortcuts(): Observable<Shortcut[]> {
return this.doGet(`${this.apiURL}/core/shortcuts`);
}
getSearchablePlugins(): Observable<PluginDescription[]> { getSearchablePlugins(): Observable<PluginDescription[]> {
return this.doGet<PluginDescription[]>(`${this.apiURL}/core/plugins`); return this.doGet<PluginDescription[]>(`${this.apiURL}/core/plugins`);
} }

View File

@ -66,6 +66,11 @@ export interface MainView {
binary_image: string; binary_image: string;
} }
export interface Shortcut {
action: string;
shortcut: string[];
}
export interface SystemInformation { export interface SystemInformation {
websocket_port: number; websocket_port: number;
login_required: boolean; login_required: boolean;

View File

@ -0,0 +1,109 @@
import { DOCUMENT } from '@angular/common';
import { EventEmitter, Inject, Injectable } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { Observable } from 'rxjs';
import { OpenLPService } from './openlp.service';
export class Shortcuts {
previousSlide = ['Up', 'PgUp'];
nextSlide = ['Down', 'PgDown'];
previousItem = ['Left'];
nextItem = ['Right'];
showDisplay = ['Space'];
themeDisplay = ['t'];
blankDisplay = ['.'];
desktopDisplay = ['d'];
}
interface Options {
element: any;
keys: string;
}
@Injectable({ providedIn: 'root' })
export class ShortcutsService {
defaults: Partial<Options> = {
element: this.document
};
constructor(
@Inject(DOCUMENT) private document: Document,
private eventManager: EventManager,
private openlpService: OpenLPService) {
this.shortcutsChanged$ = new EventEmitter<Shortcuts>();
}
private shortcuts: Shortcuts
public shortcutsChanged$: EventEmitter<Shortcuts>;
getShortcuts(useShortcutsFromOpenlp: boolean) {
const shortcuts: Shortcuts = new Shortcuts()
if (useShortcutsFromOpenlp) {
this.openlpService.getShortcuts().subscribe(res => {
res.forEach((shortcut) => {
switch (shortcut.action) {
case 'blankScreen':
shortcuts.blankDisplay = shortcut.shortcut;
break;
case 'desktopScreen':
shortcuts.desktopDisplay = shortcut.shortcut;
break;
case 'nextItem_live':
shortcuts.nextSlide = shortcut.shortcut;
break;
case 'nextService':
shortcuts.nextItem = shortcut.shortcut;
break;
case 'previousItem_live':
shortcuts.previousSlide = shortcut.shortcut;
break;
case 'previousService':
shortcuts.previousItem = shortcut.shortcut;
break;
case 'showScreen':
shortcuts.showDisplay = shortcut.shortcut;
break;
case 'themeScreen':
shortcuts.themeDisplay = shortcut.shortcut;
break;
}
})
this._handleShortcutsChanged(shortcuts);
})
} else {
this._handleShortcutsChanged(shortcuts);
}
}
addShortcut(options: Partial<Options>) {
const merged = { ...this.defaults, ...options };
const event = `keydown.${merged.keys}`;
return new Observable(observer => {
const handler = (e: KeyboardEvent) => {
const activeElement = this.document.activeElement;
const notOnInput = activeElement?.tagName !== 'INPUT' && activeElement?.tagName !== 'TEXTAREA';
if (notOnInput) {
if (document.URL.endsWith('/slides')) {
e.preventDefault();
observer.next(e);
}
}
};
const dispose = this.eventManager.addEventListener(
merged.element, event, handler
);
return () => {
dispose();
};
});
}
protected _handleShortcutsChanged(shortcuts: Shortcuts) {
this.shortcuts = shortcuts;
this.shortcutsChanged$.emit(this.shortcuts);
}
}

802
yarn.lock

File diff suppressed because it is too large Load Diff