diff --git a/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts b/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts index 3a9aecc18..c82b5ab59 100644 --- a/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts +++ b/src/app/config/setup/octoprint-authentication/octoprint-authentication.component.ts @@ -48,7 +48,7 @@ export class OctoprintAuthenticationComponent { private sendLoginRequest(): void { this.authService.startAuthProcess(this.octoprintURL).subscribe( token => { - this.notificationService.setNotification( + this.notificationService.setInfo( $localize`:@@login-request-sent:Login request send!`, $localize`:@@login-request-sent-message:Please confirm the request via the popup in the OctoPrint WebUI.`, ); diff --git a/src/app/event.service.ts b/src/app/event.service.ts index ef629aaca..d68ad1ac5 100644 --- a/src/app/event.service.ts +++ b/src/app/event.service.ts @@ -3,8 +3,9 @@ import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { ConfigService } from './config/config.service'; -import { PrinterEvent } from './model/event.model'; +import { PrinterEvent, PrinterNotification } from './model/event.model'; import { SocketService } from './services/socket/socket.service'; +import { NotificationService } from './notification/notification.service'; @Injectable() export class EventService implements OnDestroy { @@ -15,35 +16,89 @@ export class EventService implements OnDestroy { public constructor( private socketService: SocketService, private configService: ConfigService, + private notificationService: NotificationService, private router: Router, ) { this.subscriptions.add( - this.socketService.getEventSubscribable().subscribe((event: PrinterEvent) => { - if (event === PrinterEvent.PRINTING || event === PrinterEvent.PAUSED) { - setTimeout(() => { - this.printing = true; - }, 500); + this.socketService.getEventSubscribable().subscribe((event: PrinterEvent | PrinterNotification) => { + if (this.isPrinterNotification(event)) { + this.handlePrinterNotification(event); } else { - setTimeout(() => { - this.printing = false; - }, 1000); - } - - if (event === PrinterEvent.CLOSED) { - this.router.navigate(['/standby']); - } else if (event === PrinterEvent.CONNECTED) { - setTimeout(() => { - if (this.configService.isTouchscreen()) { - this.router.navigate(['/main-screen']); - } else { - this.router.navigate(['/main-screen-no-touch']); - } - }, 1000); + this.handlePrinterEvent(event); } }), ); } + // https://stackoverflow.com/questions/14425568/interface-type-check-with-typescript + // https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript/8511350#8511350 + private isPrinterNotification(object: any): object is PrinterNotification { + return typeof object === 'object' + && object !== null + && ('text' in object || 'message' in object || 'action' in object); + } + + private handlePrinterNotification(event: PrinterNotification): void { + const messages = { + 'FilamentRunout T0': $localize`:@@prompt-filament-runout-t0:Filament runout detected. Ejecting filament, please wait...`, + 'Nozzle Parked': $localize`:@@prompt-filament-runout:A filament runout has been detected. Please remove the ejected filament, insert filament from a new spool and press Continue.`, + 'Continue': $localize`:@@prompt-continue:Continue`, + 'Paused': $localize`:@@prompt-filament-runout-resume:The filament has been primed. Do you want to continue printing?`, + 'PurgeMore': $localize`:@@prompt-filament-runout-purge:Purge more filament`, + 'Heater Timeout': $localize`:@@prompt-heater-timeout:The hotend has been disabled due to inactivity, to avoid burning the filament. Press Reheat when ready to resume.`, + 'Reheat': $localize`:@@prompt-reheat:Reheat`, + 'Reheat Done': $localize`:@@prompt-reheat-done:The hotend is now ready.`, + }; + if (event.action === 'close') { + this.notificationService.closeNotification(); + } else if (event.choices?.length > 0) { + // event is action:prompt + this.notificationService.setPrompt( + $localize`:@@action-required:Action required`, + messages[event.text] || event.text, + event.choices.map(c => messages[c] || c) + ); + } else if (event.choices?.length == 0) { + // event is action:prompt without choices + this.notificationService.setWarning( + $localize`:@@printer-information:Printer information`, + messages[event.text] || event.text + ); + } else { + // event is action:notification + // this.notificationService.setInfo( + // $localize`:@@printer-information:Printer information`, + // messages[event.message] || event.message + // ); + // TODO: annoying as a notification + // should be put with an autoclear timeout in the bottom-right statusline + } + } + + private handlePrinterEvent(event: PrinterEvent): void { + if (event === PrinterEvent.PRINTING || event === PrinterEvent.PAUSED) { + setTimeout(() => { + this.printing = true; + }, 500); + } else { + setTimeout(() => { + this.printing = false; + }, 1000); + } + + if (event === PrinterEvent.CLOSED) { + this.router.navigate(['/standby']); + } else if (event === PrinterEvent.CONNECTED) { + setTimeout(() => { + if (this.configService.isTouchscreen()) { + this.router.navigate(['/main-screen']); + } else { + this.router.navigate(['/main-screen-no-touch']); + } + }, 1000); + } + } + ngOnDestroy(): void { this.subscriptions.unsubscribe(); } diff --git a/src/app/model/event.model.ts b/src/app/model/event.model.ts index 21f30cffa..46cdee9bd 100644 --- a/src/app/model/event.model.ts +++ b/src/app/model/event.model.ts @@ -6,3 +6,11 @@ export enum PrinterEvent { IDLE, UNKNOWN, } + +// either notification (message) or prompt (action, text and choices) +export interface PrinterNotification { + message?: string, + action?: string, + text?: string, + choices?: string[], +} diff --git a/src/app/model/system.model.ts b/src/app/model/system.model.ts index afc37d66b..d10480e4b 100644 --- a/src/app/model/system.model.ts +++ b/src/app/model/system.model.ts @@ -2,6 +2,7 @@ export interface Notification { heading: string; text: string; type: string; + choices?: string[]; closed: () => void; } diff --git a/src/app/notification/notification.component.html b/src/app/notification/notification.component.html index d2159874e..6a1191980 100644 --- a/src/app/notification/notification.component.html +++ b/src/app/notification/notification.component.html @@ -1,5 +1,6 @@
@@ -7,3 +8,22 @@ {{ notification.text }} tap this card to close it
+ +
+
{{ notification.heading }}
+
{{ notification.text }}
+
+
+ {{ choice }} +
+
+
diff --git a/src/app/notification/notification.component.scss b/src/app/notification/notification.component.scss index 8f4110c1b..bc9195efd 100644 --- a/src/app/notification/notification.component.scss +++ b/src/app/notification/notification.component.scss @@ -1,54 +1,94 @@ -.notification { - display: block; - position: fixed; - width: 80vw; - background-color: #5a6675; - border-radius: 1.3vw; - left: 10vw; - top: -75vh; - transition: top 0.7s ease-in-out; - box-shadow: 0 4px 10px -3px rgba(0, 0, 0, 0.75); - z-index: 100; - border-right: 1vw solid #5a6675; - - &__show { - top: 5vh; - } - - &__heading { - display: block; - text-align: center; - font-size: 3.5vw; - font-weight: 500; - margin-top: 4vh; - } - - &__text { - font-size: 2.7vw; - padding: 5vh 3vw 1vh; - display: block; - text-align: center; - } - - &__close { - font-size: 2vw; - display: block; - text-align: center; - padding-bottom: 2vh; - opacity: 0.4; - } - - &__border { - &-error { - border-left: 1vw solid #c23616; - } - - &-warn { - border-left: 1vw solid #fbc531; - } - - &-notification { - border-left: 1vw solid #4bae50; - } - } -} +.notification { + display: block; + position: fixed; + width: 80vw; + background-color: #5a6675; + border-radius: 1.3vw; + left: 10vw; + top: -75vh; + transition: top 0.7s ease-in-out; + box-shadow: 0 4px 10px -3px rgba(0, 0, 0, 0.75); + z-index: 100; + border-right: 1vw solid #5a6675; + + &-prompt { + position: fixed; + width: 100vw; + height: 100vh; + left: 0vw; + top: -100vh; + transition: top 0.7s ease-in-out; + background-color: #5a6675; + z-index: 100; + display: flex; + flex-direction: column; + justify-content: space-around; + + &__show { + top: 0vh; + } + + &__choices { + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: center; + padding: 0vw 5vw; + } + + &__choice { + display: block; + font-size: 3vw; + background-color: #2196f3; + text-align: center; + padding: 3vh; + border-radius: 0.8vw; + margin: 1vw; + + &-first { + background-color: #44bd32; + } + } + } + + &__show { + top: 5vh; + } + + &__heading { + display: block; + text-align: center; + font-size: 3.5vw; + font-weight: 500; + margin-top: 4vh; + } + + &__text { + font-size: 2.7vw; + padding: 5vh 3vw 1vh; + display: block; + text-align: center; + } + + &__close { + font-size: 2vw; + display: block; + text-align: center; + padding-bottom: 2vh; + opacity: 0.4; + } + + &__border { + &-error { + border-left: 1vw solid #c23616; + } + + &-warn { + border-left: 1vw solid #fbc531; + } + + &-info { + border-left: 1vw solid #4bae50; + } + } +} diff --git a/src/app/notification/notification.component.ts b/src/app/notification/notification.component.ts index a0b306129..1f57799f9 100644 --- a/src/app/notification/notification.component.ts +++ b/src/app/notification/notification.component.ts @@ -1,7 +1,10 @@ import { Component, NgZone, OnDestroy } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { Subscription } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { Notification } from '../model'; +import { ConfigService } from '../config/config.service'; import { NotificationService } from './notification.service'; @Component({ @@ -16,11 +19,17 @@ export class NotificationComponent implements OnDestroy { heading: '', text: '', type: '', + choices: null, closed: null, }; public show = false; - public constructor(private notificationService: NotificationService, private zone: NgZone) { + public constructor( + private notificationService: NotificationService, + private zone: NgZone, + private http: HttpClient, + private configService: ConfigService, + ) { this.subscriptions.add( this.notificationService .getObservable() @@ -35,6 +44,21 @@ export class NotificationComponent implements OnDestroy { } } + public answerPrompt(index: number): void { + this.http + .post( + this.configService.getApiURL('plugin/action_command_prompt'), + { command: 'select', choice: index }, + this.configService.getHTTPHeaders() + ) + .pipe( + catchError(error => + this.notificationService.setError($localize`:@@error-answer-prompt:Can't answer prompt!`, error.message), + ), + ) + .subscribe(); + } + private setNotification(notification: Notification | 'close'): void { this.zone.run(() => { if (notification === 'close') { diff --git a/src/app/notification/notification.service.ts b/src/app/notification/notification.service.ts index 22b69ab58..9cf956ecf 100644 --- a/src/app/notification/notification.service.ts +++ b/src/app/notification/notification.service.ts @@ -49,13 +49,25 @@ export class NotificationService { }); } - public setNotification(heading: string, text: string): Promise { + public setInfo(heading: string, text: string): Promise { return new Promise(resolve => { if (this.observer) { - this.observer.next({ heading, text, type: 'notification', closed: resolve }); + this.observer.next({ heading, text, type: 'info', closed: resolve }); } else { setTimeout(() => { - this.setNotification(heading, text); + this.setInfo(heading, text); + }, 1000); + } + }); + } + + public setPrompt(heading: string, text: string, choices: string[]): Promise { + return new Promise(resolve => { + if (this.observer) { + this.observer.next({ heading, text, type: 'prompt', choices, closed: resolve }); + } else { + setTimeout(() => { + this.setPrompt(heading, text, choices); }, 1000); } }); diff --git a/src/app/services/socket/socket.octoprint.service.ts b/src/app/services/socket/socket.octoprint.service.ts index b51124c56..469359900 100644 --- a/src/app/services/socket/socket.octoprint.service.ts +++ b/src/app/services/socket/socket.octoprint.service.ts @@ -7,7 +7,7 @@ import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { ConfigService } from '../../config/config.service'; import { ConversionService } from '../../conversion.service'; -import { JobStatus, PrinterEvent, PrinterState, PrinterStatus, SocketAuth } from '../../model'; +import { JobStatus, PrinterEvent, PrinterNotification, PrinterState, PrinterStatus, SocketAuth } from '../../model'; import { DisplayLayerProgressData, OctoprintFilament, @@ -25,7 +25,7 @@ export class OctoPrintSocketService implements SocketService { private printerStatusSubject: Subject; private jobStatusSubject: Subject; - private eventSubject: Subject; + private eventSubject: Subject; private printerStatus: PrinterStatus; private jobStatus: JobStatus; @@ -39,7 +39,7 @@ export class OctoPrintSocketService implements SocketService { ) { this.printerStatusSubject = new ReplaySubject(); this.jobStatusSubject = new Subject(); - this.eventSubject = new ReplaySubject(); + this.eventSubject = new ReplaySubject(); } //==== SETUP & AUTH ====// @@ -116,6 +116,27 @@ export class OctoPrintSocketService implements SocketService { this.socket.next(payload); } + private handlePluginMessage(pluginMessage: OctoprintPluginMessage) { + const plugins = [ + { + check: (plugin: string) => plugin === 'DisplayLayerProgress-websocket-payload' + && this.configService.isDisplayLayerProgressEnabled(), + handler: (data: unknown) => { + this.extractFanSpeed(data as DisplayLayerProgressData); + this.extractLayerHeight(data as DisplayLayerProgressData); + }, + }, + { + check: (plugin: string) => ['action_command_prompt', 'action_command_notification'].includes(plugin), + handler: (data: unknown) => this.eventSubject.next(data as PrinterNotification), + }, + ]; + + plugins.forEach(plugin => + plugin.check(pluginMessage.plugin.plugin) && plugin.handler(pluginMessage.plugin.data) + ); + } + private setupSocket(resolve: () => void) { this.socket.subscribe( message => { @@ -125,14 +146,7 @@ export class OctoPrintSocketService implements SocketService { } else if (Object.hasOwnProperty.bind(message)('event')) { this.extractPrinterEvent(message as OctoprintSocketEvent); } else if (Object.hasOwnProperty.bind(message)('plugin')) { - const pluginMessage = message as OctoprintPluginMessage; - if ( - pluginMessage.plugin.plugin === 'DisplayLayerProgress-websocket-payload' && - this.configService.isDisplayLayerProgressEnabled() - ) { - this.extractFanSpeed(pluginMessage.plugin.data as DisplayLayerProgressData); - this.extractLayerHeight(pluginMessage.plugin.data as DisplayLayerProgressData); - } + this.handlePluginMessage(message as OctoprintPluginMessage); } else if (Object.hasOwnProperty.bind(message)('reauth')) { this.systemService.getSessionKey().subscribe(socketAuth => this.authenticateSocket(socketAuth)); } else if (Object.hasOwnProperty.bind(message)('connected')) { @@ -314,7 +328,7 @@ export class OctoPrintSocketService implements SocketService { return this.jobStatusSubject.pipe(startWith(this.jobStatus)); } - public getEventSubscribable(): Observable { + public getEventSubscribable(): Observable { return this.eventSubject; } } diff --git a/src/app/services/socket/socket.service.ts b/src/app/services/socket/socket.service.ts index 259fa5ceb..47d1f532e 100644 --- a/src/app/services/socket/socket.service.ts +++ b/src/app/services/socket/socket.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { JobStatus, PrinterEvent, PrinterStatus } from '../../model'; +import { JobStatus, PrinterEvent, PrinterNotification, PrinterStatus } from '../../model'; @Injectable() export abstract class SocketService { @@ -11,5 +11,5 @@ export abstract class SocketService { abstract getJobStatusSubscribable(): Observable; - abstract getEventSubscribable(): Observable; + abstract getEventSubscribable(): Observable; } diff --git a/src/locale/messages.fr.xlf b/src/locale/messages.fr.xlf index 591881d2a..9e54e6e2c 100644 --- a/src/locale/messages.fr.xlf +++ b/src/locale/messages.fr.xlf @@ -290,6 +290,46 @@ do you want to execute the following command? voulez-vous exécuter la commande suivante ? + + Filament runout detected. Ejecting filament, please wait... + Fin de filament détectée. Ejection du filament, veuillez patienter... + + + A filament runout has been detected. Please remove the ejected filament, insert filament from a new spool and press Continue. + Une fin de filament a été détectée. Veuillez retirer le filament éjecté, insérer le filament d'une nouvelle bobine et presser Continuer. + + + Continue + Continuer + + + The filament has been primed. Do you want to continue printing? + Le filament a été amorcé. Souhaitez-vous continuer l'impression ? + + + Purge more filament + Purger plus de filament + + + The hotend has been disabled due to inactivity, to avoid burning the filament. Press Reheat when ready to resume. + La buse a été désactivée car l'imprimante était inactive, pour éviter de surchauffer le filament. Appuyez sur 'Activer la chauffe' pour reprendre + + + Reheat + Activer la chauffe + + + The hotend is now ready. + La buse est à température. + + + Action required + Action requise + + + Printer information + Information de l'imprimante + load new filament charger un nouveau filament @@ -534,6 +574,10 @@ tap this card to close it touchez cette carte pour la fermer + + Can't answer prompt! + Impossible de répondre à la demande! + cancel annuler