UNPKG

nodecg

Version:

Dynamic broadcast graphics rendered in a browser

401 lines (334 loc) 8.4 kB
import "@polymer/iron-icons/device-icons.js"; import "@polymer/iron-icons/iron-icons.js"; import "@polymer/paper-button/paper-button.js"; import "./ncg-graphic-instance-diff"; import * as Polymer from "@polymer/polymer"; import { MutableData } from "@polymer/polymer/lib/mixins/mutable-data"; import type { NodeCG } from "../../../../types/nodecg"; const pulseElement = document.createElement("div"); setInterval(() => { pulseElement.dispatchEvent( new CustomEvent("pulse", { detail: { timestamp: Date.now(), }, }), ); }, 1000); /** * @customElement * @polymer * @appliesMixin MutableData */ class NcgGraphicInstance extends MutableData(Polymer.PolymerElement) { static get template() { return Polymer.html` <style include="nodecg-theme"> :host { @apply --layout-flex-none; @apply --layout-horizontal; box-sizing: border-box; font-size: 18px; font-style: normal; font-weight: 500; height: 35px; line-height: normal; margin-bottom: 5px; margin-left: 8px; margin-right: 8px; position: relative; white-space: nowrap; } :host(:first-child) { margin-top: 5px; } #indicator { @apply --layout-flex-none; background-color: #CACACA; width: 8px; } :host([status="nominal"]) #indicator { background-color: #00A651; } :host([status="out-of-date"]) #indicator { background-color: #FFC700; } #indicatorIcon { @apply --layout-center-center; @apply --layout-flex-none; @apply --layout-horizontal; background: #525F78; border-right: 1px solid #6F7D99; width: 38px; } :host([status="nominal"]) #indicatorIcon { color: #00A651; } :host([status="out-of-date"]) #indicatorIcon, :host([status="out-of-date"]) #status { color: #FFC700; } :host([status="out-of-date"]) #status { cursor: help; } #ip, #status, #duration { @apply --layout-center; @apply --layout-horizontal; background: #525F78; padding: 0 16px; } #ip { @apply --layout-flex; border-right: 1px solid #6F7D99; min-width: 0; } #ip-actual { overflow: hidden; text-overflow: ellipsis; } #status { @apply --layout-flex-none; border-right: 1px solid #6F7D99; width: 187px; } #duration { @apply --layout-flex-none; width: 120px; margin-right: 1px; } #duration-icon { margin-right: 6px; margin-top: -1px; } #reloadButton { --nodecg-background-color: #6155BD; } #killButton { --nodecg-background-color: #FF7575; } #diff { transition: opacity 150ms ease-in-out; z-index: 1; top: 38px; left: 40px } paper-button { @apply --layout-center-center; @apply --layout-flex-none; @apply --layout-horizontal; border-radius: 0; margin: 0 1px; min-width: 0; padding: 0; width: 40px; } paper-button:last-child { margin-right: 0; } :host([responsive-mode="medium"]) #status { width: 94px; } :host([responsive-mode="medium"]) #duration { width: 100px; } :host(:not([responsive-mode="wide"])) #ip, :host(:not([responsive-mode="wide"])) #status, :host(:not([responsive-mode="wide"])) #duration { padding: 0 12px; } :host([responsive-mode="narrow"]) #status { width: 91px; } :host([responsive-mode="narrow"]) #duration { width: 69px; } :host([responsive-mode="narrow"]) #status, :host([responsive-mode="narrow"]) #duration-icon { display: none; } :host([responsive-mode="narrow"]) #diff { left: 0; } :host([status="closed"]) { opacity: 0; transition: opacity 0.9s ease-out; } :host(:not([status="out-of-date"])) #diff { display: none; } :host([status="out-of-date"][status-hover]) #diff { opacity: 1; pointer-events: initial; } :host(:not([status-hover])) #diff { display: none; opacity: 0; pointer-events: none; } </style> <div id="indicator"></div> <div id="indicatorIcon"> <iron-icon icon="[[_calcIndicatorIcon(status)]]"></iron-icon> </div> <div id="ip"> <div id="ip-actual" title="[[instance.ipv4]]">[[instance.ipv4]]</div> </div> <div id="status">[[_calcStatusMessage(status, responsiveMode)]]</div> <div id="duration"> <iron-icon id="duration-icon" icon="device:access-time"></iron-icon> <span id="duration-text">[[_calcTimeSince(instance.timestamp, _pulse)]]</span> </div> <paper-button id="reloadButton" on-tap="reload"> <iron-icon icon="refresh"></iron-icon> </paper-button> <paper-button id="killButton" on-tap="kill" hidden="[[!graphic.singleInstance]]"> <iron-icon icon="close"></iron-icon> </paper-button> <ncg-graphic-instance-diff id="diff" instance="[[instance]]"></ncg-graphic-instance-diff> `; } static get is() { return "ncg-graphic-instance"; } static get properties() { return { responsiveMode: { type: String, reflectToAttribute: true, }, graphic: Object, instance: Object, status: { type: String, reflectToAttribute: true, computed: "_computeStatus(instance)", }, statusHover: { type: Boolean, reflectToAttribute: true, value: false, }, }; } override ready(): void { super.ready(); pulseElement.addEventListener("pulse", (e: any) => { this._pulse = e.detail.timestamp; }); const showDiff = () => { clearTimeout(this._offTimeout); this._offTimeout = null; this.statusHover = true; }; const hideDiff = (immediate: boolean) => { if (immediate) { clearTimeout(this._offTimeout); this._offTimeout = null; this.statusHover = false; } else if (!this._offTimeout) { this._offTimeout = setTimeout(() => { clearTimeout(this._offTimeout); this._offTimeout = null; this.statusHover = false; }, 250); } }; this.$.indicatorIcon.addEventListener("mouseenter", () => { if (this.responsiveMode === "narrow") { showDiff(); } }); this.$.status.addEventListener("mouseenter", showDiff); this.$.diff.addEventListener("mouseenter", showDiff); this.$.indicatorIcon.addEventListener("mouseleave", () => { if (this.responsiveMode === "narrow") { hideDiff(false); } }); this.$.status.addEventListener("mouseleave", () => { hideDiff(false); }); this.$.diff.addEventListener("mouseleave", () => { hideDiff(false); }); this.$.diff.addEventListener("close", () => { hideDiff(true); }); } reload() { this.$.reloadButton.disabled = true; window.socket.emit("graphic:requestRefresh", this.instance, () => { this.$.reloadButton.disabled = false; }); } kill() { this.$.killButton.disabled = true; window.socket.emit("graphic:requestKill", this.instance, () => { this.$.killButton.disabled = false; }); } _computeStatus(instance?: NodeCG.GraphicsInstance) { if (!instance) { return ""; } if (!instance.open) { return "closed"; } return instance.potentiallyOutOfDate ? "out-of-date" : "nominal"; } _calcIndicatorIcon(status: string) { switch (status) { case "nominal": return "check"; case "out-of-date": return "warning"; default: return "close"; } } _calcStatusMessage(status: string, responsiveMode: string) { switch (status) { case "nominal": return "Latest"; case "out-of-date": return responsiveMode === "wide" ? "Potentially Out of Date" : "Out of Date"; case "closed": return "Closed"; default: return "Error"; } } _calcTimeSince(timestamp: number) { return timeSince(timestamp); } } customElements.define(NcgGraphicInstance.is, NcgGraphicInstance); /* istanbul ignore next: not really easy or that important to test */ function timeSince(date: number) { const seconds = Math.floor(new Date().getTime() / 1000 - date / 1000); let interval = Math.floor(seconds / 31536000); if (interval > 1) { return `${interval} year`; } interval = Math.floor(seconds / 2592000); if (interval > 1) { return `${interval} month`; } interval = Math.floor(seconds / 86400); if (interval >= 1) { return `${interval} day`; } interval = Math.floor(seconds / 3600); if (interval >= 1) { return `${interval} hour`; } interval = Math.floor(seconds / 60); if (interval > 1) { return `${interval} min`; } return `${Math.floor(seconds)} sec`; }