import "@webcomponents/webcomponentsjs/webcomponents-loader"; import "@webcomponents/shadycss/apply-shim.min.js"; import "@polymer/app-layout/app-layout.js"; import "@polymer/app-route/app-location.js"; import "@polymer/app-route/app-route.js"; import "@polymer/iron-flex-layout/iron-flex-layout.js"; import "@polymer/iron-icons/av-icons.js"; import "@polymer/iron-icons/communication-icons.js"; import "@polymer/iron-icons/image-icons.js"; import "@polymer/iron-icons/iron-icons.js"; import "@polymer/iron-image/iron-image.js"; import "@polymer/iron-media-query/iron-media-query.js"; import "@polymer/iron-pages/iron-pages.js"; import "@polymer/iron-selector/iron-selector.js"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js"; import "@polymer/paper-dialog/paper-dialog.js"; import "@polymer/paper-icon-button/paper-icon-button.js"; import "@polymer/paper-item/paper-icon-item.js"; import "@polymer/paper-tabs/paper-tabs.js"; import "@polymer/paper-toast/paper-toast.js"; import "@polymer/polymer/lib/elements/custom-style.js"; import "../css/nodecg-theme"; import "./assets/ncg-assets"; import "./graphics/ncg-graphics"; import "./mixer/ncg-mixer"; import "./ncg-dialog"; import "./ncg-workspace"; import "./settings/ncg-settings"; import * as Polymer from "@polymer/polymer"; import { timeOut } from "@polymer/polymer/lib/utils/async.js"; import { Debouncer } from "@polymer/polymer/lib/utils/debounce.js"; import type { NodeCG } from "../../../types/nodecg"; class NcgDashboard extends Polymer.PolymerElement { static get template() { return Polymer.html`
`; } static get is() { return "ncg-dashboard"; } static get properties() { return { route: { type: Object, observer: "_routeChanged", }, smallScreen: { type: Boolean, observer: "_smallScreenChanged", }, loginDisabled: { type: Boolean, value: !window.ncgConfig.login?.enabled, }, bundles: { type: Array, value: window.__renderData__.bundles, }, workspaces: { type: Array, value: window.__renderData__.workspaces, }, dialogs: { type: Array, computed: "_computeDialogs(bundles)", }, pages: { type: Array, value() { const pages = [ { name: "Graphics", route: "graphics", icon: "visibility", }, { name: "Mixer", route: "mixer", icon: "av:volume-up", }, { name: "Assets", route: "assets", icon: "file-upload", }, ]; // For the time being, the "Settings" button is only relevant // when login security is enabled. if (window.ncgConfig.login?.enabled) { pages.push({ name: "Settings", route: "settings", icon: "settings", }); } return pages; }, }, }; } override ready(): void { super.ready(); // Images are stored as data URIs so that they can be displayed even with no connection to the server let FAIL_URI: string; let SUCCESS_URI: string; let notified = false; getImageDataURI("img/notifications/standard/fail.png", (err, result) => { /* istanbul ignore if: hard-to-hit error */ if (err) { console.error(err); } else { FAIL_URI = result!.data; } }); getImageDataURI("img/notifications/standard/success.png", (err, result) => { /* istanbul ignore if: hard-to-hit error */ if (err) { console.error(err); } else { SUCCESS_URI = result!.data; } }); window.socket.on("protocol_error", (err) => { /* istanbul ignore next: coverage is buggy here */ if (err.type === "UnauthorizedError") { window.location.href = `/authError?code=${err.code}&message=${err.message}`; } else { console.error("Unhandled socket error:", err); this.$.mainToast.show("Unhandled socket error!"); } }); window.socket.on("disconnect", () => { this.$.mainToast.show("Lost connection to NodeCG server!"); notified = false; this.disconnected = true; }); window.socket.io.on("reconnect_attempt", (attempts) => { if (!this.$.reconnectToast.opened) { this.$.reconnectToast.open(); } if (attempts >= 3 && !notified) { notified = true; notify("Disconnected", { body: "The dashboard has lost connection with NodeCG.", icon: FAIL_URI, tag: "disconnect", }); } }); window.socket.io.on("reconnect", (attempts) => { this.$.mainToast.show("Reconnected to NodeCG server!"); this.$.reconnectToast.hide(); this.disconnected = false; if (attempts >= 3) { notify("Reconnected", { body: `Successfully reconnected on attempt # ${attempts}`, icon: SUCCESS_URI, tag: "reconnect", }); } }); window.socket.io.on("reconnect_failed", () => { this.$.mainToast.show("Failed to reconnect to NodeCG server!"); notify("Reconnection Failed", { body: "Could not reconnect to NodeCG after the maximum number of attempts.", icon: FAIL_URI, tag: "reconnect_failed", }); }); } override connectedCallback(): void { super.connectedCallback(); // If the default workspace is hidden (due to it having no panels), // show the next workspace by default. if ( this.route.path === "" && window.__renderData__.workspaces[0]!.route !== "" ) { window.location.hash = window.__renderData__.workspaces[0]!.route; } if (!this.routeData) { this.routeData = {}; } if (!this.routeData.page) { this.set("routeData.page", ""); } this._fixTabs(); } /* istanbul ignore next: can't cover since it navigates the page */ logout() { window.location.href = "/logout"; } closeDrawer() { this.$.drawer.close(); } _smallScreenChanged(newVal: boolean) { if (!newVal) { this.closeDrawer(); } } _equal(a: any, b: any) { return a === b; } _selectRoute(e: any) { window.location.hash = e.target.closest("paper-tab").route; } _routeChanged() { this._fixTabs(); this._fixPathDebounce = Debouncer.debounce( this._fixPathDebounce, timeOut.after(100), this._fixPath.bind(this), ); } _fixTabs() { // For some reason, our paper-tabs elements need a little help // to know when the route has changed and when they should deselect their tabs. const tabs = this.shadowRoot!.querySelectorAll("paper-tabs"); if (tabs) { tabs.forEach((tabSet) => { if (tabSet.selected !== this.route.path) { tabSet.selected = this.route.path; } }); } } _fixPath() { // If the current hash points to a route that doesn't exist, (such as // after a refresh which removed a workspace), default to the first workspace. if (!this.$.pages.selectedItem) { window.location.hash = window.__renderData__.workspaces[0]!.route; } } _computeDialogs(bundles: NodeCG.Bundle[]) { const dialogs: any[] = []; bundles.forEach((bundle) => { bundle.dashboard.panels.forEach((panel) => { if (panel.dialog) { dialogs.push(panel); } }); }); return dialogs; } _falsey(value: any) { return !value; } _calcButtonClass(buttonType: string) { return buttonType === "confirm" ? "nodecg-accept" : "nodecg-reject"; } } function getImageDataURI( url: string, cb: ( error: NodeJS.ErrnoException | undefined, result?: { image: HTMLImageElement; data: string }, ) => void, ) { let data; let canvas; let ctx; const img = new Image(); img.onload = function () { // Create the canvas element. canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; // Get '2d' context and draw the image. ctx = canvas.getContext("2d"); if (!ctx) { cb(new Error("Could not create canvas context")); return; } ctx.drawImage(img, 0, 0); // Get canvas data URL try { data = canvas.toDataURL(); cb(undefined, { image: img, data, }); } catch (e: any) { /* istanbul ignore next: hard-to-test error */ cb(e); } canvas.remove(); }; // Load image URL. try { img.src = url; } catch (e: any) { /* istanbul ignore next: hard-to-test error */ cb(e); } } function notify( title: string, options: { body?: string; icon?: string; tag?: string } = {}, ) { // Let's check if the browser supports notifications if (!("Notification" in window)) { return; } // Let's check if the user is okay to get some notification. // Otherwise, we need to ask the user for permission. // Note, Chrome does not implement the permission static property. // So we have to check for NOT 'denied' instead of 'default'. if (window.Notification.permission === "granted") { // If it's okay let's create a notification const notification = new window.Notification(title, options); setTimeout(() => { notification.close(); }, 5000); } else if (window.Notification.permission !== "denied") { void window.Notification.requestPermission((permission) => { // If the user is okay, let's create a notification if (permission === "granted") { const notification = new window.Notification(title, options); setTimeout( (n) => { n.close(); }, 5000, notification, ); } }); } // At last, if the user already denied any notification, and you // want to be respectful there is no need to bother them any more. } customElements.define("ncg-dashboard", NcgDashboard);