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`
[[workspace.label]]
[[page.name]]
`;
}
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);