diff --git a/.jshintignore b/.jshintignore index 56c80727..8c4a12b2 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,3 +1,5 @@ _datafiles/html/public/static/js/xterm.4.19.0.js _datafiles/html/public/static/js/xterm-addon-fit.js -_datafiles/html/admin/static/js/htmx.2.0.3.js \ No newline at end of file +_datafiles/html/public/static/js/winbox.bundle.min.js +_datafiles/html/admin/static/js/htmx.2.0.3.js + diff --git a/_datafiles/html/public/static/js/gmcp.js b/_datafiles/html/public/static/js/gmcp.js new file mode 100644 index 00000000..1b48a632 --- /dev/null +++ b/_datafiles/html/public/static/js/gmcp.js @@ -0,0 +1,337 @@ +class RoomGridSVG { + constructor(selector, options = {}) { + // ── Configurable options & defaults ─────────────────────────────── + this.cellSize = options.cellSize || 100; + this.cellMargin = options.cellMargin || 20; + this.spacing = this.cellSize + this.cellMargin; + this.zoomStep = options.zoomStep || 1.2; + this.zoomLevel = options.initialZoom || 1; + this.onRoomClick = options.onRoomClick || (() => {}); + this.zoomButtonSize = options.zoomButtonSize || 25; + this.controlsMargin = options.controlsMargin || 10; + this.roomEdgeColor = options.roomEdgeColor || "#1c6b60"; + this.visitingColor = options.visitingColor || "#c20000"; + // ── Internal state ──────────────────────────────────────────────── + // rooms: Map + this.rooms = new Map(); + this.drawnEdges = new Set(); // to avoid dup lines + this.currentCenterId = null; // for highlight + + // ── Build container & SVG ───────────────────────────────────────── + this.container = document.querySelector(selector); + this.container.style.position = 'relative'; + + this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); + this.svg.style.width = '100%'; + this.svg.style.height = '100%'; + this.container.appendChild(this.svg); + + // Connections under rooms: + this.connectionsGroup = document.createElementNS(this.svg.namespaceURI, 'g'); + this.svg.appendChild(this.connectionsGroup); + // Rooms on top: + this.roomsGroup = document.createElementNS(this.svg.namespaceURI, 'g'); + this.svg.appendChild(this.roomsGroup); + + // Default tiny viewBox until rooms exist: + this.svg.setAttribute('viewBox', '0 0 1 1'); + + // ── HTML overlay zoom controls ──────────────────────────────────── + this._createHTMLControls(); + } + + // ── Public API ─────────────────────────────────────────────────────── + + /** + * Add or update a room. + * - Pre-adds any Exits given as {RoomId,x,y,…} + * - If room already exists, updates its position, color, text, & redraws edges. + */ + addRoom(room) { + const id = room.RoomId; + + // 1) Pre-add exit-defined rooms + if (Array.isArray(room.Exits)) { + room.Exits.forEach(e => { + if (e && typeof e === 'object' && e.RoomId != null) { + + if (this.rooms.has(e.RoomId)) return; + + this.addRoom({ + RoomId: e.RoomId, + Text: e.Text != null ? e.Text : String(e.RoomId), + x: e.x, + y: e.y, + Exits: Array.isArray(e.Exits) ? e.Exits : [] + }); + } + }); + } + + // prepare defaults + const defaultColor = room.Color || '#fff'; + const displayText = room.Text != null ? + room.Text : + String(room.RoomId); + + // 2) UPDATE existing + if (this.rooms.has(id)) { + const entry = this.rooms.get(id); + // update stored data + entry.room.x = room.x; + entry.room.y = room.y; + entry.room.Exits = Array.isArray(room.Exits) ? room.Exits : []; + entry.room.Color = room.Color; + entry.room.Text = room.Text; + entry.defaultColor = defaultColor; + + // move & recolor rect + const rect = this.svg.querySelector(`rect[data-room-rect="${id}"]`); + rect.setAttribute('x', room.x * this.spacing); + rect.setAttribute('y', room.y * this.spacing); + if (this.currentCenterId === id) { + rect.setAttribute('fill', this.visitingColor); + } else { + rect.setAttribute('fill', defaultColor); + } + + // move & update label + const txtEl = this.svg.querySelector(`g[data-room-id="${id}"] text`); + txtEl.setAttribute('x', room.x * this.spacing + this.cellSize / 2); + txtEl.setAttribute('y', room.y * this.spacing + this.cellSize / 2 + 5); + txtEl.textContent = displayText; + + // redraw any new edges + this._drawEdgesForRoom(id); + + // refresh bounds & view + this._updateBounds(); + this._applyZoom(); + return; + } + + // 3) NEW room → draw group + const g = document.createElementNS(this.svg.namespaceURI, 'g'); + g.setAttribute('data-room-id', id); + + // square + const rect = document.createElementNS(this.svg.namespaceURI, 'rect'); + rect.setAttribute('width', this.cellSize); + rect.setAttribute('height', this.cellSize); + rect.setAttribute('x', room.x * this.spacing); + rect.setAttribute('y', room.y * this.spacing); + rect.setAttribute('stroke', this.roomEdgeColor); + rect.setAttribute('stroke-width', '4'); + rect.setAttribute('rx', this.cellSize / 10); // corner radius X + rect.setAttribute('ry', this.cellSize / 10); // corner radius Y + rect.setAttribute('data-room-rect', id); + rect.setAttribute('fill', defaultColor); + rect.style.cursor = 'pointer'; + rect.addEventListener('click', () => this.onRoomClick(room)); + g.appendChild(rect); + + // label + const label = document.createElementNS(this.svg.namespaceURI, 'text'); + label.setAttribute('x', room.x * this.spacing + this.cellSize / 2); + label.setAttribute('y', room.y * this.spacing + this.cellSize / 2 + 5); + label.setAttribute('text-anchor', 'middle'); + label.setAttribute('font-size', this.cellSize * 0.3); + label.textContent = displayText; + g.appendChild(label); + + this.roomsGroup.appendChild(g); + this.rooms.set(id, { + room, + group: g, + defaultColor + }); + + // draw edges for this new room + this._drawEdgesForRoom(id); + + // refresh bounds & view + this._updateBounds(); + this._applyZoom(); + } + + /** + * Bulk‐set rooms (wipes existing). + */ + setRooms(arr) { + this.reset(); + arr.forEach(r => this.addRoom(r)); + } + + /** + * Clear everything. + */ + reset() { + this.rooms.clear(); + this.drawnEdges.clear(); + this.currentCenterId = null; + this.zoomLevel = 1; + this.svg.setAttribute('viewBox', '0 0 1 1'); + this.roomsGroup.innerHTML = ''; + this.connectionsGroup.innerHTML = ''; + } + + /** + * Center & highlight a room. Previous one reverts to its default color. + */ + centerOnRoom(id) { + const entry = this.rooms.get(id); + if (!entry) return; + + // un-highlight previous + if (this.currentCenterId != null) { + const prevRect = this.svg.querySelector( + `rect[data-room-rect="${this.currentCenterId}"]` + ); + if (prevRect) { + const prevEntry = this.rooms.get(this.currentCenterId); + prevRect.setAttribute('fill', prevEntry.defaultColor); + } + } + + // compute new view center + this.center = { + x: entry.room.x * this.spacing + this.cellSize / 2, + y: entry.room.y * this.spacing + this.cellSize / 2 + }; + this._applyZoom(); + + // highlight new + const newRect = this.svg.querySelector( + `rect[data-room-rect="${id}"]` + ); + if (newRect) newRect.setAttribute('fill', this.visitingColor); + + this.currentCenterId = id; + } + + zoomIn() { + this.zoomLevel *= this.zoomStep; + this._applyZoom(); + } + zoomOut() { + this.zoomLevel /= this.zoomStep; + this._applyZoom(); + } + + drawConnection(a, b) { + if (!this.rooms.has(a) || !this.rooms.has(b)) return; + this._drawEdge(a, b); + this._applyZoom(); + } + + // ── Private draw helpers ─────────────────────────────────────────────── + + _createHTMLControls() { + const div = document.createElement('div'); + div.style.cssText = ` + position:absolute; + top:${this.controlsMargin}px; + right:${this.controlsMargin}px; + display:flex; gap:5px; + `; + const mk = (lbl, cb) => { + const b = document.createElement('button'); + b.textContent = lbl; + b.style.cssText = ` + width:${this.zoomButtonSize}px; + height:${this.zoomButtonSize}px; + font-size:${this.zoomButtonSize*0.6}px; + line-height:1; + `; + b.addEventListener('click', cb); + return b; + }; + div.append(mk('−', () => this.zoomOut()), mk('+', () => this.zoomIn())); + this.container.appendChild(div); + } + + _drawEdgesForRoom(id) { + const me = this.rooms.get(id) + .room; + const exits = Array.isArray(me.Exits) ? me.Exits : []; + + // draw its own exits + exits.forEach(e => { + const to = (typeof e === 'object') ? e.RoomId : e; + if (this.rooms.has(to)) this._drawEdge(id, to); + }); + + // draw others’ exits back to it + this.rooms.forEach(({ + room + }, otherId) => { + if (otherId === id) return; + const oe = Array.isArray(room.Exits) ? room.Exits : []; + if (oe.some(x => ((typeof x === 'object') ? x.RoomId : x) === id)) { + this._drawEdge(otherId, id); + } + }); + } + + _drawEdge(a, b) { + const key = a < b ? `${a}-${b}` : `${b}-${a}`; + if (this.drawnEdges.has(key)) return; + this.drawnEdges.add(key); + + const ra = this.rooms.get(a) + .room; + const rb = this.rooms.get(b) + .room; + const x1 = ra.x * this.spacing + this.cellSize / 2; + const y1 = ra.y * this.spacing + this.cellSize / 2; + const x2 = rb.x * this.spacing + this.cellSize / 2; + const y2 = rb.y * this.spacing + this.cellSize / 2; + + const line = document.createElementNS(this.svg.namespaceURI, 'line'); + line.setAttribute('x1', x1); + line.setAttribute('y1', y1); + line.setAttribute('x2', x2); + line.setAttribute('y2', y2); + line.setAttribute('stroke', this.roomEdgeColor); + line.setAttribute('stroke-width', '20'); + this.connectionsGroup.appendChild(line); + } + + _updateBounds() { + if (!this.rooms.size) { + this.bounds = { + minX: 0, + maxX: 0, + minY: 0, + maxY: 0 + }; + } else { + const xs = [...this.rooms.values()].map(e => e.room.x); + const ys = [...this.rooms.values()].map(e => e.room.y); + this.bounds = { + minX: Math.min(...xs), + maxX: Math.max(...xs), + minY: Math.min(...ys), + maxY: Math.max(...ys) + }; + } + this.worldWidth = (this.bounds.maxX - this.bounds.minX + 1) * this.spacing; + this.worldHeight = (this.bounds.maxY - this.bounds.minY + 1) * this.spacing; + + if (!this.center && this.rooms.size) { + this.center = { + x: this.bounds.minX * this.spacing + this.worldWidth / 2, + y: this.bounds.minY * this.spacing + this.worldHeight / 2 + }; + } + } + + _applyZoom() { + const hw = this.worldWidth / (2 * this.zoomLevel); + const hh = this.worldHeight / (2 * this.zoomLevel); + const x0 = (this.center ? this.center.x : this.worldWidth / 2) - hw; + const y0 = (this.center ? this.center.y : this.worldHeight / 2) - hh; + this.svg.setAttribute('viewBox', `${x0} ${y0} ${hw*2} ${hh*2}`); + } +} diff --git a/_datafiles/html/public/static/js/winbox.bundle.min.js b/_datafiles/html/public/static/js/winbox.bundle.min.js new file mode 100644 index 00000000..538e9394 --- /dev/null +++ b/_datafiles/html/public/static/js/winbox.bundle.min.js @@ -0,0 +1,34 @@ +/** + * WinBox.js v0.2.82 (Bundle) + * Author and Copyright: Thomas Wilkerling + * Licence: Apache-2.0 + * Hosted by Nextapps GmbH + * https://github.com/nextapps-de/winbox + */ +(function(){'use strict';var e,aa=document.createElement("style");aa.innerHTML="@keyframes wb-fade-in{0%{opacity:0}to{opacity:.85}}.winbox{position:fixed;left:0;top:0;background:#0050ff;box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22);transition:width .3s,height .3s,left .3s,top .3s;transition-timing-function:cubic-bezier(.3,1,.3,1);contain:layout size;text-align:left;touch-action:none}.wb-body,.wb-header{position:absolute;left:0}.wb-header{top:0;width:100%;height:35px;line-height:35px;color:#fff;overflow:hidden;z-index:1}.wb-body{top:35px;right:0;bottom:0;overflow:auto;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;will-change:contents;background:#fff;margin-top:0!important;contain:strict;z-index:0}.wb-control *,.wb-icon{background-repeat:no-repeat}.wb-drag{height:100%;padding-left:10px;cursor:move}.wb-title{font-family:Arial,sans-serif;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wb-icon{display:none;width:20px;height:100%;margin:-1px 8px 0-3px;float:left;background-size:100%;background-position:center}.wb-e,.wb-w{width:10px;top:0}.wb-n,.wb-s{left:0;height:10px;position:absolute}.wb-n{top:-5px;right:0;cursor:n-resize;z-index:2}.wb-e{position:absolute;right:-5px;bottom:0;cursor:w-resize;z-index:2}.wb-s{bottom:-5px;right:0;cursor:n-resize;z-index:2}.wb-nw,.wb-sw,.wb-w{left:-5px}.wb-w{position:absolute;bottom:0;cursor:w-resize;z-index:2}.wb-ne,.wb-nw,.wb-sw{width:15px;height:15px;z-index:2;position:absolute}.wb-nw{top:-5px;cursor:nw-resize}.wb-ne,.wb-sw{cursor:ne-resize}.wb-ne{top:-5px;right:-5px}.wb-se,.wb-sw{bottom:-5px}.wb-se{position:absolute;right:-5px;width:15px;height:15px;cursor:nw-resize;z-index:2}.wb-control{float:right;height:100%;max-width:100%;text-align:center}.wb-control *{display:inline-block;width:30px;height:100%;max-width:100%;background-position:center;cursor:pointer}.no-close .wb-close,.no-full .wb-full,.no-header .wb-header,.no-max .wb-max,.no-min .wb-min,.no-resize .wb-body~div,.wb-body .wb-hide,.wb-show,.winbox.hide,.winbox.min .wb-body>*,.winbox.min .wb-full,.winbox.min .wb-min,.winbox.modal .wb-full,.winbox.modal .wb-max,.winbox.modal .wb-min{display:none}.winbox.max .wb-drag,.winbox.min .wb-drag{cursor:default}.wb-min{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAyIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOCAwaDdhMSAxIDAgMCAxIDAgMkgxYTEgMSAwIDAgMSAwLTJoN3oiLz48L3N2Zz4=);background-size:14px auto;background-position:center calc(50% + 6px)}.wb-max{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCA5NiA5NiI+PHBhdGggZD0iTTIwIDcxLjMxMUMxNS4zNCA2OS42NyAxMiA2NS4yMyAxMiA2MFYyMGMwLTYuNjMgNS4zNy0xMiAxMi0xMmg0MGM1LjIzIDAgOS42NyAzLjM0IDExLjMxMSA4SDI0Yy0yLjIxIDAtNCAxLjc5LTQgNHY1MS4zMTF6Ii8+PHBhdGggZD0iTTkyIDc2VjM2YzAtNi42My01LjM3LTEyLTEyLTEySDQwYy02LjYzIDAtMTIgNS4zNy0xMiAxMnY0MGMwIDYuNjMgNS4zNyAxMiAxMiAxMmg0MGM2LjYzIDAgMTItNS4zNyAxMi0xMnptLTUyIDRjLTIuMjEgMC00LTEuNzktNC00VjM2YzAtMi4yMSAxLjc5LTQgNC00aDQwYzIuMjEgMCA0IDEuNzkgNCA0djQwYzAgMi4yMS0xLjc5IDQtNCA0SDQweiIvPjwvc3ZnPg==);background-size:17px auto}.wb-close{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xIC0xIDE4IDE4Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJtMS42MTMuMjEuMDk0LjA4M0w4IDYuNTg1IDE0LjI5My4yOTNsLjA5NC0uMDgzYTEgMSAwIDAgMSAxLjQwMyAxLjQwM2wtLjA4My4wOTRMOS40MTUgOGw2LjI5MiA2LjI5M2ExIDEgMCAwIDEtMS4zMiAxLjQ5N2wtLjA5NC0uMDgzTDggOS40MTVsLTYuMjkzIDYuMjkyLS4wOTQuMDgzQTEgMSAwIDAgMSAuMjEgMTQuMzg3bC4wODMtLjA5NEw2LjU4NSA4IC4yOTMgMS43MDdBMSAxIDAgMCAxIDEuNjEzLjIxeiIvPjwvc3ZnPg==);background-size:15px auto;background-position:5px center}.wb-full{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjIuNSIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNOCAzSDVhMiAyIDAgMCAwLTIgMnYzbTE4IDBWNWEyIDIgMCAwIDAtMi0yaC0zbTAgMThoM2EyIDIgMCAwIDAgMi0ydi0zTTMgMTZ2M2EyIDIgMCAwIDAgMiAyaDMiLz48L3N2Zz4=);background-size:16px auto}.winbox.max .wb-body~div,.winbox.min .wb-body~div,.winbox.modal .wb-body~div,.winbox.modal .wb-drag,body.wb-lock iframe{pointer-events:none}.winbox.max{box-shadow:none}.winbox.max .wb-body{margin:0!important}.winbox iframe{position:absolute;width:100%;height:100%;border:0}body.wb-lock .winbox{will-change:left,top,width,height;transition:none}.winbox.modal:before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:inherit;border-radius:inherit}.winbox.modal:after{content:'';position:absolute;top:-50vh;left:-50vw;right:-50vw;bottom:-50vh;background:#0d1117;animation:wb-fade-in .2s ease-out forwards;z-index:-1}.no-animation{transition:none}.no-shadow{box-shadow:none}.no-header .wb-body{top:0}.no-move:not(.min) .wb-title{pointer-events:none}.wb-body .wb-show{display:revert}"; +var h=document.getElementsByTagName("head")[0];h.firstChild?h.insertBefore(aa,h.firstChild):h.appendChild(aa);var ba=document.createElement("div");ba.innerHTML="
";function k(a,b,c,f){a&&a.addEventListener(b,c,f||!1)}function l(a,b){var c=window,f=m;c&&c.removeEventListener(a,b,f||!1)}function t(a,b){a.stopPropagation();b&&a.preventDefault()}function u(a,b,c){c=""+c;a["_s_"+b]!==c&&(a.style.setProperty(b,c),a["_s_"+b]=c)};/* + self.max &&*/ +var x=[],A=[],ca={capture:!0,passive:!1},m={capture:!0,passive:!0},B,da=0,E=10,F,J,ha,K,P,ia; +function U(a,b){if(!(this instanceof U))return new U(a);B||ja();if(a){if(b){var c=a;a=b}if("string"===typeof a)c=a;else{var f=a.id;var d=a.index;var n=a.root;var p=a.template;c=c||a.title;var v=a.icon;var L=a.mount;var Q=a.html;var g=a.url;var q=a.width;var r=a.height;var w=a.minwidth;var C=a.minheight;var y=a.maxwidth;var z=a.maxheight;var ea=a.autosize;var D=a.overflow;var G=a.min;var H=a.max;var I=a.hidden;var fa=a.modal;var X=a.x||(fa?"center":0);var Y=a.y||(fa?"center":0);var M=a.top;var N=a.left; +var R=a.bottom;var S=a.right;var la=a.background;var O=a.border;var T=a.header;var Z=a["class"];var ma=a.oncreate;var ra=a.onclose;var sa=a.onfocus;var ta=a.onblur;var ua=a.onmove;var va=a.onresize;var wa=a.onfullscreen;var xa=a.onmaximize;var ya=a.onminimize;var za=a.onrestore;var Aa=a.onhide;var Ba=a.onshow;var Ca=a.onload}}this.g=(p||ba).cloneNode(!0);this.g.id=this.id=f||"winbox-"+ ++da;this.g.className="winbox"+(Z?" "+("string"===typeof Z?Z:Z.join(" ")):"")+(fa?" modal":"");this.g.winbox=this; +this.window=this.g;this.body=this.g.getElementsByClassName("wb-body")[0];this.h=T||35;A.push(this);la&&this.setBackground(la);O?u(this.body,"margin",O+(isNaN(O)?"":"px")):O=0;T&&(b=this.g.getElementsByClassName("wb-header")[0],u(b,"height",T+"px"),u(b,"line-height",T+"px"),u(this.body,"top",T+"px"));c&&this.setTitle(c);v&&this.setIcon(v);L?this.mount(L):Q?this.body.innerHTML=Q:g&&this.setUrl(g,Ca);M=M?V(M,P):0;R=R?V(R,P):0;N=N?V(N,K):0;S=S?V(S,K):0;c=K-N-S;v=P-M-R;y=y?V(y,c):c;z=z?V(z,v):v;w=w?V(w, +y):150;C=C?V(C,z):this.h;ea?((n||B).appendChild(this.body),q=Math.max(Math.min(this.body.clientWidth+2*O+1,y),w),r=Math.max(Math.min(this.body.clientHeight+this.h+O+1,z),C),this.g.appendChild(this.body)):(q=q?V(q,y):Math.max(y/2,w)|0,r=r?V(r,z):Math.max(z/2,C)|0);X=X?V(X,c,q):N;Y=Y?V(Y,v,r):M;this.x=X;this.y=Y;this.width=q;this.height=r;this.s=w;this.o=C;this.m=y;this.l=z;this.top=M;this.right=S;this.bottom=R;this.left=N;this.index=d;this.j=D;this.focused=this.hidden=this.full=this.max=this.min=!1; +this.onclose=ra;this.onfocus=sa;this.onblur=ta;this.onmove=ua;this.onresize=va;this.onfullscreen=wa;this.onmaximize=xa;this.onminimize=ya;this.onrestore=za;this.onhide=Aa;this.onshow=Ba;I?this.hide():this.focus();if(d||0===d)this.index=d,u(this.g,"z-index",d),d>E&&(E=d);H?this.maximize():G?this.minimize():this.resize().move();ka(this);(n||B).appendChild(this.g);ma&&ma.call(this,a)}U["new"]=function(a){return new U(a)};U.stack=function(){return A}; +function V(a,b,c){"string"===typeof a&&("center"===a?a=(b-c)/2+.5|0:"right"===a||"bottom"===a?a=b-c:(c=parseFloat(a),a="%"===(""+c!==a&&a.substring((""+c).length))?b/100*c+.5|0:c));return a} +function ja(){B=document.body;B[J="requestFullscreen"]||B[J="msRequestFullscreen"]||B[J="webkitRequestFullscreen"]||B[J="mozRequestFullscreen"]||(J="");ha=J&&J.replace("request","exit").replace("mozRequest","mozCancel").replace("Request","Exit");k(window,"resize",function(){na();oa()});k(B,"mousedown",function(){ia=!1},!0);k(B,"mousedown",function(){if(!ia){var a=A.length;if(a)for(--a;0<=a;a--){var b=A[a];if(b.focused){b.blur();break}}}});na()} +function ka(a){W(a,"drag");W(a,"n");W(a,"s");W(a,"w");W(a,"e");W(a,"nw");W(a,"ne");W(a,"se");W(a,"sw");k(a.g.getElementsByClassName("wb-min")[0],"click",function(b){t(b);a.min?a.restore().focus():a.minimize()});k(a.g.getElementsByClassName("wb-max")[0],"click",function(b){t(b);a.max?a.restore().focus():a.maximize().focus()});J?k(a.g.getElementsByClassName("wb-full")[0],"click",function(b){t(b);a.fullscreen().focus()}):a.addClass("no-full");k(a.g.getElementsByClassName("wb-close")[0],"click",function(b){t(b); +a.close()||(a=null)});k(a.g,"mousedown",function(){ia=!0},!0);k(a.body,"mousedown",function(){a.focus()},!0)}function pa(a){x.splice(x.indexOf(a),1);oa();a.removeClass("min");a.min=!1;a.g.title=""}function oa(){for(var a=x.length,b={},c={},f=0,d;fr){a.max?a.restore():a.maximize();return}}}a.min||(B.classList.add("wb-lock"),(p=g.touches)&&(p=p[0])?(g=p,k(window,"touchmove",f,m),k(window,"touchend",d,m)):(k(window,"mousemove",f,m),k(window,"mouseup",d,m)),v=g.pageX,L=g.pageY)}function f(g){t(g);p&&(g=g.touches[0]);var q=g.pageX;g=g.pageY;var r=q-v,w=g-L,C=a.width,y=a.height,z=a.x, +ea=a.y,D;if("drag"===b){if(a.g.classList.contains("no-move"))return;a.x+=r;a.y+=w;var G=D=1}else{if("e"===b||"se"===b||"ne"===b){a.width+=r;var H=1}else if("w"===b||"sw"===b||"nw"===b)a.x+=r,a.width-=r,G=H=1;if("s"===b||"se"===b||"sw"===b){a.height+=w;var I=1}else if("n"===b||"ne"===b||"nw"===b)a.y+=w,a.height-=w,D=I=1}H&&(a.width=Math.max(Math.min(a.width,a.m,K-a.x-a.right),a.s),H=a.width!==C);I&&(a.height=Math.max(Math.min(a.height,a.l,P-a.y-a.bottom),a.o),I=a.height!==y);(H||I)&&a.resize();G&& +(a.max&&(a.x=(qK/3*2?K-a.width-a.right:K/2-a.width/2)+r),a.x=Math.max(Math.min(a.x,a.j?K-30:K-a.width-a.right),a.j?30-a.width:a.left),G=a.x!==z);D&&(a.max&&(a.y=a.top+w),a.y=Math.max(Math.min(a.y,a.j?P-a.h:P-a.height-a.bottom),a.top),D=a.y!==ea);if(G||D)a.max&&a.restore(),a.move();if(H||G)v=q;if(I||D)L=g}function d(g){t(g);B.classList.remove("wb-lock");p?(l("touchmove",f),l("touchend",d)):(l("mousemove",f),l("mouseup",d))}var n=a.g.getElementsByClassName("wb-"+b)[0];if(n){var p,v,L, +Q=0;k(n,"mousedown",c,ca);k(n,"touchstart",c,ca)}}function na(){var a=document.documentElement;K=a.clientWidth;P=a.clientHeight}e=U.prototype;e.mount=function(a){this.unmount();a.i||(a.i=a.parentNode);this.body.textContent="";this.body.appendChild(a);return this};e.unmount=function(a){var b=this.body.firstChild;if(b){var c=a||b.i;c&&c.appendChild(b);b.i=a}return this}; +e.setTitle=function(a){var b=this.g.getElementsByClassName("wb-title")[0];a=this.title=a;var c=b.firstChild;c?c.nodeValue=a:b.textContent=a;return this};e.setIcon=function(a){var b=this.g.getElementsByClassName("wb-icon")[0];u(b,"background-image","url("+a+")");u(b,"display","inline-block");return this};e.setBackground=function(a){u(this.g,"background",a);return this}; +e.setUrl=function(a,b){var c=this.body.firstChild;c&&"iframe"===c.tagName.toLowerCase()?c.src=a:(this.body.innerHTML='',b&&(this.body.firstChild.onload=b));return this};e.focus=function(a){if(!1===a)return this.blur();if(!this.focused){a=A.length;if(1 + + @@ -197,6 +199,7 @@

Volume Controls

+ + +
+
+
+
+ 100% +
+
+
+ 100% +
+
+ +
+
diff --git a/modules/gmcp/gmcp.go b/modules/gmcp/gmcp.go index 8c760004..c3ee6727 100644 --- a/modules/gmcp/gmcp.go +++ b/modules/gmcp/gmcp.go @@ -30,9 +30,9 @@ var ( GmcpAccept = term.TerminalCommand{Chars: []byte{term.TELNET_IAC, term.TELNET_DO, TELNET_GMCP}, EndChars: []byte{}} // Indicates the client accepts GMCP sub-negotiations. GmcpRefuse = term.TerminalCommand{Chars: []byte{term.TELNET_IAC, term.TELNET_DONT, TELNET_GMCP}, EndChars: []byte{}} // Indicates the client refuses GMCP sub-negotiations. - GmcpPayload = term.TerminalCommand{Chars: []byte{term.TELNET_IAC, term.TELNET_SB, TELNET_GMCP}, EndChars: []byte{term.TELNET_IAC, term.TELNET_SE}} // Wrapper for sending GMCP payloads - - gmcpModule GMCPModule = GMCPModule{} + GmcpPayload = term.TerminalCommand{Chars: []byte{term.TELNET_IAC, term.TELNET_SB, TELNET_GMCP}, EndChars: []byte{term.TELNET_IAC, term.TELNET_SE}} // Wrapper for sending GMCP payloads + GmcpWebPayload = term.TerminalCommand{Chars: []byte("!!GMCP("), EndChars: []byte{')'}} // Wrapper for sending GMCP payloads + gmcpModule GMCPModule = GMCPModule{} ) // //////////////////////////////////////////////////////////////////// @@ -69,7 +69,6 @@ func init() { func isGMCPEnabled(connectionId uint64) bool { //return true - if gmcpData, ok := gmcpModule.cache.Get(connectionId); ok { return gmcpData.GMCPAccepted } @@ -143,6 +142,7 @@ func (g *GMCPModule) onNetConnect(n plugins.NetConnection) { setting := GMCPSettings{} setting.Client.Name = `WebClient` setting.Client.Version = `1.0.0` + setting.GMCPAccepted = true g.cache.Add(n.ConnectionId(), setting) return } @@ -375,10 +375,11 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { return events.Continue } + var gmcpSettings GMCPSettings + var ok bool if !isGMCPEnabled(connId) { - gmcpSettings, ok := g.cache.Get(connId) + gmcpSettings, ok = g.cache.Get(connId) if !ok { - gmcpSettings = GMCPSettings{} g.cache.Add(connId, gmcpSettings) @@ -391,6 +392,11 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { if !gmcpSettings.GMCPAccepted { return events.Continue } + } else { + gmcpSettings, ok = g.cache.Get(connId) + if !ok { + return events.Continue + } } switch v := gmcp.Payload.(type) { @@ -410,7 +416,12 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { v = append([]byte(gmcp.Module+` `), v...) } - connections.SendTo(GmcpPayload.BytesWithPayload(v), connId) + if gmcpSettings.Client.Name == `WebClient` { + connections.SendTo(GmcpWebPayload.BytesWithPayload(v), connId) + } else { + connections.SendTo(GmcpPayload.BytesWithPayload(v), connId) + } + case string: // DEBUG ONLY @@ -427,7 +438,12 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { v = gmcp.Module + ` ` + v } - connections.SendTo(GmcpPayload.BytesWithPayload([]byte(v)), connId) + if gmcpSettings.Client.Name == `WebClient` { + connections.SendTo(GmcpWebPayload.BytesWithPayload([]byte(v)), connId) + } else { + connections.SendTo(GmcpPayload.BytesWithPayload([]byte(v)), connId) + } + default: payload, err := json.Marshal(gmcp.Payload) if err != nil { @@ -449,7 +465,12 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { payload = append([]byte(gmcp.Module+` `), payload...) } - connections.SendTo(GmcpPayload.BytesWithPayload(payload), connId) + if gmcpSettings.Client.Name == `WebClient` { + connections.SendTo(GmcpWebPayload.BytesWithPayload(payload), connId) + } else { + connections.SendTo(GmcpPayload.BytesWithPayload(payload), connId) + } + } return events.Continue