Make use of the configured shortcuts in OpenLP.

This commit is contained in:
Chris Witterholt 2024-04-02 04:54:12 +00:00 committed by Raoul Snyman
parent ed9db1d295
commit 7ad8daff62
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"
},
"dependencies": {
"@angular/animations": "^17.3.1",
"@angular/cdk": "^17.3.1",
"@angular/common": "^17.3.1",
"@angular/compiler": "^17.3.1",
"@angular/core": "^17.3.1",
"@angular/forms": "^17.3.1",
"@angular/material": "^17.3.1",
"@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^17.3.1",
"@angular/router": "^17.3.1",
"@fontsource/roboto": "^5.0.8",
"core-js": "^3.35.1",
"@angular/animations": "^17.3.2",
"@angular/cdk": "^17.3.2",
"@angular/common": "^17.3.2",
"@angular/compiler": "^17.3.2",
"@angular/core": "^17.3.2",
"@angular/forms": "^17.3.2",
"@angular/material": "^17.3.2",
"@angular/platform-browser": "^17.3.2",
"@angular/platform-browser-dynamic": "^17.3.2",
"@angular/router": "^17.3.2",
"@fontsource/roboto": "^5.0.12",
"core-js": "^3.36.1",
"hammerjs": "^2.0.8",
"material-icons": "^1.13.12",
"rxjs": "^7.8.1",
"zone.js": "^0.14.3"
"zone.js": "^0.14.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.1",
"@angular-devkit/build-angular": "^17.3.2",
"@angular-eslint/builder": "^17.3.0",
"@angular-eslint/eslint-plugin": "^17.3.0",
"@angular-eslint/eslint-plugin-template": "^17.3.0",
"@angular-eslint/schematics": "^17.3.0",
"@angular-eslint/template-parser": "^17.3.0",
"@angular/cli": "~17.3.1",
"@angular/compiler-cli": "^17.3.1",
"@angular/language-service": "^17.3.1",
"@angular/cli": "~17.3.2",
"@angular/compiler-cli": "^17.3.2",
"@angular/language-service": "^17.3.2",
"@chiragrupani/karma-chromium-edge-launcher": "^2.3.1",
"@types/jasmine": "~5.1.4",
"@types/jasminewd2": "~2.0.13",
"@types/node": "~20.11.17",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"@types/node": "~20.12.2",
"@typescript-eslint/eslint-plugin": "7.4.0",
"@typescript-eslint/parser": "7.4.0",
"browserslist": "^4.23.0",
"browserslist-useragent-regexp": "^4.1.1",
"eslint": "^8.56.0",
"eslint": "^8.57.0",
"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",
"jasmine-core": "~5.1.2",
"jasmine-spec-reporter": "~7.0.0",
@ -71,7 +71,7 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"ts-node": "~10.9.2",
"typescript": "~5.3.3"
"typescript": "~5.4.3"
},
"private": true
}

View File

@ -10,7 +10,8 @@ import { LoginComponent } from './components/login/login.component';
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 { Shortcuts, ShortcutsService } from './shortcuts.service';
import { ShortcutPipe } from './components/shortcuts/shortcut.pipe';
import { SettingsService } from './settings.service';
import * as supportedBrowsers from '../assets/supportedBrowsers';
@ -29,14 +30,16 @@ export class AppComponent implements OnInit {
appVersion = '0.0';
webSocketOpen = false;
fastSwitching = false;
useShortcutsFromOpenlp = false;
constructor(private pageTitleService: PageTitleService, private openlpService: OpenLPService,
private dialog: MatDialog, private bottomSheet: MatBottomSheet, private windowRef: WindowRef,
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);
this.appVersion = windowRef.nativeWindow.appVersion || '0.0';
private shortcutsService: ShortcutsService, private settingsService: SettingsService) {
this.pageTitleService.pageTitleChanged$.subscribe(pageTitle => this.pageTitle = pageTitle);
this.openlpService.stateChanged$.subscribe(item => this.state = item);
this.openlpService.webSocketStateChanged$.subscribe(status => this.webSocketOpen = status === WebSocketStatus.Open);
this.shortcutsService.shortcutsChanged$.subscribe(shortcuts => this.addShortcuts(shortcuts));
this.appVersion = this.windowRef.nativeWindow.appVersion || '0.0';
this.webSocketOpen = openlpService.webSocketStatus === WebSocketStatus.Open;
// 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
@ -49,47 +52,60 @@ export class AppComponent implements OnInit {
if (!(supportedBrowsers.test(navigator.userAgent))) {
window.location.replace("/assets/notsupported.html");
}
this.openlpService.retrieveSystemInformation().subscribe(res => this.showLogin = res.login_required);
this.addHotKeys();
this.openlpService.retrieveSystemInformation().subscribe(res => {
this.showLogin = res.login_required
this.useShortcutsFromOpenlp = this.openlpService.assertApiVersionMinimum(2, 5)
this.shortcutsService.getShortcuts(this.useShortcutsFromOpenlp);
}
);
this.fastSwitching = this.settingsService.get('fastSwitching');
this.settingsService.onPropertyChanged('fastSwitching').subscribe(value => this.fastSwitching = value);
}
addHotKeys(): void {
this.hotKeysService.addShortcut({ keys: 'ArrowUp' }).subscribe(() =>
this.previousSlide()
);
this.hotKeysService.addShortcut({ keys: 'ArrowDown' }).subscribe(() =>
this.nextSlide()
);
this.hotKeysService.addShortcut({ keys: 'PageUp' }).subscribe(() =>
this.previousSlide()
);
this.hotKeysService.addShortcut({ keys: 'PageDown' }).subscribe(() =>
this.nextSlide()
);
this.hotKeysService.addShortcut({ keys: 'ArrowLeft' }).subscribe(() =>
this.previousItem()
);
this.hotKeysService.addShortcut({ keys: 'ArrowRight' }).subscribe(() =>
this.nextItem()
);
this.hotKeysService.addShortcut({ keys: 'Space' }).subscribe(() =>
{
addShortcuts(shortcuts: Shortcuts): void {
const shortcutPipe = new ShortcutPipe();
shortcuts.previousSlide.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.previousSlide()
)
});
shortcuts.nextSlide.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.nextSlide()
)
});
shortcuts.previousItem.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.previousItem()
)
});
shortcuts.nextItem.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.nextItem()
)
});
shortcuts.showDisplay.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() => {
if (this.state.displayMode !== DisplayMode.Presentation) {
this.showDisplay();
}
}
);
this.hotKeysService.addShortcut({ keys: 't' }).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()
);
this.hotKeysService.addShortcut({ keys: 'd' }).subscribe(() =>
this.state.displayMode === DisplayMode.Desktop ? this.showDisplay() : this.desktopDisplay()
);
})
});
shortcuts.themeDisplay.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
this.state.displayMode === DisplayMode.Theme ? this.showDisplay() : this.themeDisplay()
)
});
shortcuts.blankDisplay.forEach((key) => {
this.shortcutsService.addShortcut({ keys: shortcutPipe.transform(key) }).subscribe(() =>
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 {

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

View File

@ -66,6 +66,11 @@ export interface MainView {
binary_image: string;
}
export interface Shortcut {
action: string;
shortcut: string[];
}
export interface SystemInformation {
websocket_port: number;
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