Add fonts. Update UI to open/close and select a sequence.

This commit is contained in:
mmcwilliams 2024-08-01 11:57:49 -04:00
parent 38c61e78cb
commit 1447a7aad4
16 changed files with 194 additions and 49 deletions

View File

@ -1,7 +1,8 @@
interface SequenceState { interface SequenceState {
hash : string, hash : string,
name : string, name : string,
progress : number progress : number,
current? : number
} }
interface State { interface State {
@ -13,4 +14,5 @@ interface State {
interface Message { interface Message {
cmd? : string; cmd? : string;
state? : State; state? : State;
sequence? : string;
} }

View File

@ -36,6 +36,9 @@ class Client {
case 'open' : case 'open' :
this.receiveCameraOpen(); this.receiveCameraOpen();
break; break;
case 'close' :
this.receiveCameraClose();
break;
default: default:
console.warn(`No command ${id}`); console.warn(`No command ${id}`);
break; break;
@ -50,6 +53,43 @@ class Client {
private receiveCameraOpen () { private receiveCameraOpen () {
console.log('got camera open'); console.log('got camera open');
} }
public sendCameraClose () {
console.log('send camera close');
this.client.send(JSON.stringify({ cmd : 'close' }));
}
private receiveCameraClose () {
console.log('got camera close');
}
public sendSelect () {
const sequence : string = (document.getElementById('sequence') as HTMLSelectElement ).value;
let msg : Message;
if (sequence === '- Select Image Sequence -') {
return;
}
msg = { cmd : 'select', sequence };
console.log('send select');
console.log(sequence)
this.client.send(JSON.stringify(msg));
}
public fullscreen () {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
this.exitFullscreen();
}
}
public exitFullscreen () {
if (document.fullscreenElement) {
document.exitFullscreen()
}
}
} }
const client : Client = new Client(); const client : Client = new Client();

View File

@ -39,10 +39,7 @@ class CameraSerialPortMock extends serialport_1.SerialPortMock {
this._mockSend(Commands.CAMERA_OPEN, 125); this._mockSend(Commands.CAMERA_OPEN, 125);
break; break;
case Commands.CAMERA_CLOSE: case Commands.CAMERA_CLOSE:
this._mockSend(Commands.CAMERA_FORWARD, 3); this._mockSend(Commands.CAMERA_CLOSE, 125);
break;
case Commands.CAMERA_OPEN:
this._mockSend(Commands.CAMERA_BACKWARD, 2);
break; break;
default: default:
this.log.warn(`[MOCK] Command "${buffer}" does not exist on mock`); this.log.warn(`[MOCK] Command "${buffer}" does not exist on mock`);

File diff suppressed because one or more lines are too long

28
dist/index.js vendored
View File

@ -37,12 +37,14 @@ const log_1 = require("./log");
const files_1 = require("./files"); const files_1 = require("./files");
const ffmpeg_1 = require("./ffmpeg"); const ffmpeg_1 = require("./ffmpeg");
const camera_1 = require("./camera"); const camera_1 = require("./camera");
const sequence_1 = require("./sequence");
const log = (0, log_1.createLog)('fm'); const log = (0, log_1.createLog)('fm');
const app = (0, express_1.default)(); const app = (0, express_1.default)();
let wss; let wss;
let fd; let fd;
let ffmpeg; let ffmpeg;
let camera; let camera;
let sequence;
let index; let index;
let port; let port;
let wsPort; let wsPort;
@ -174,22 +176,39 @@ async function onClientMessage(data, ws) {
log.error('Error parsing message', err); log.error('Error parsing message', err);
} }
if (msg !== null && typeof msg.cmd !== 'undefined') { if (msg !== null && typeof msg.cmd !== 'undefined') {
res = await cmd(msg.cmd); res = await cmd(msg);
} }
ws.send(JSON.stringify(res)); ws.send(JSON.stringify(res));
} }
async function cmd(id) { async function cmd(msg) {
switch (id) { switch (msg.cmd) {
case 'open': case 'open':
await cameraOpen(); await cameraOpen();
return { cmd: 'open' }; return { cmd: 'open' };
case 'close':
await cameraClose();
return { cmd: 'close' };
case 'select':
await select(msg.sequence);
return { cmd: 'select', sequence: msg.sequence };
default: default:
log.warn(`No matching command: ${id}`); log.warn(`No matching command: ${msg.cmd}`);
} }
} }
async function cameraOpen() { async function cameraOpen() {
await camera.open(); await camera.open();
} }
async function cameraClose() {
await camera.close();
}
async function select(id) {
const sequencesArr = await files_1.Files.enumerateSequences(sequences);
const seq = sequencesArr.find(el => el.hash === id);
if (typeof seq == 'undefined' || seq == null) {
log.error('Sequence not found, maybe deleted?', new Error(`Cannot find sequence ${id}`));
}
await sequence.load(seq);
}
app.get('/', async (req, res, next) => { app.get('/', async (req, res, next) => {
const sequencesArr = await files_1.Files.enumerateSequences(sequences); const sequencesArr = await files_1.Files.enumerateSequences(sequences);
//const videosArr : VideoObject[] = await Files.enumerateVideos(videos); //const videosArr : VideoObject[] = await Files.enumerateVideos(videos);
@ -202,6 +221,7 @@ async function main() {
ffmpeg = new ffmpeg_1.FFMPEG(process.env['FFMPEG']); ffmpeg = new ffmpeg_1.FFMPEG(process.env['FFMPEG']);
camera = new camera_1.Camera(); camera = new camera_1.Camera();
//fd = new FD(process.env['FD'], width, height, process.env['FD_HOST'], parseInt(process.env['FD_PORT'])); //fd = new FD(process.env['FD'], width, height, process.env['FD_HOST'], parseInt(process.env['FD_PORT']));
sequence = new sequence_1.Sequence();
app.listen(port, async () => { app.listen(port, async () => {
log.info(`filmout_manager HTTP server running on port ${port}`); log.info(`filmout_manager HTTP server running on port ${port}`);
}); });

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -53,10 +53,7 @@ class CameraSerialPortMock extends SerialPortMock {
this._mockSend(Commands.CAMERA_OPEN, 125); this._mockSend(Commands.CAMERA_OPEN, 125);
break; break;
case Commands.CAMERA_CLOSE : case Commands.CAMERA_CLOSE :
this._mockSend(Commands.CAMERA_FORWARD, 3); this._mockSend(Commands.CAMERA_CLOSE, 125);
break;
case Commands.CAMERA_OPEN :
this._mockSend(Commands.CAMERA_BACKWARD, 2);
break; break;
default: default:
this.log.warn(`[MOCK] Command "${buffer}" does not exist on mock`); this.log.warn(`[MOCK] Command "${buffer}" does not exist on mock`);

4
src/globals.d.ts vendored
View File

@ -1,7 +1,8 @@
interface SequenceState { interface SequenceState {
hash : string, hash : string,
name : string, name : string,
progress : number progress : number,
current? : number
} }
interface State { interface State {
@ -13,4 +14,5 @@ interface State {
interface Message { interface Message {
cmd? : string; cmd? : string;
state? : State; state? : State;
sequence? : string;
} }

View File

@ -23,6 +23,7 @@ import { delay } from './delay';
import { FD } from './fd'; import { FD } from './fd';
import { FFMPEG } from './ffmpeg'; import { FFMPEG } from './ffmpeg';
import { Camera } from './camera'; import { Camera } from './camera';
import { Sequence } from './sequence';
const log : Logger = createLog('fm'); const log : Logger = createLog('fm');
const app : Express = express(); const app : Express = express();
@ -30,6 +31,7 @@ let wss : Server;
let fd : FD; let fd : FD;
let ffmpeg : FFMPEG; let ffmpeg : FFMPEG;
let camera : Camera; let camera : Camera;
let sequence : Sequence;
let index : HandlebarsTemplateDelegate<any>; let index : HandlebarsTemplateDelegate<any>;
let port : number; let port : number;
@ -161,18 +163,24 @@ async function onClientMessage (data : any, ws : WebSocket) {
log.error('Error parsing message', err); log.error('Error parsing message', err);
} }
if (msg !== null && typeof msg.cmd !== 'undefined') { if (msg !== null && typeof msg.cmd !== 'undefined') {
res = await cmd(msg.cmd); res = await cmd(msg);
} }
ws.send(JSON.stringify(res)); ws.send(JSON.stringify(res));
} }
async function cmd (id : string) : Promise<Message> { async function cmd (msg : Message) : Promise<Message> {
switch(id) { switch(msg.cmd) {
case 'open' : case 'open' :
await cameraOpen(); await cameraOpen();
return { cmd : 'open' } return { cmd : 'open' }
case 'close' :
await cameraClose();
return { cmd : 'close' }
case 'select' :
await select(msg.sequence)
return { cmd : 'select', sequence : msg.sequence }
default : default :
log.warn(`No matching command: ${id}`); log.warn(`No matching command: ${msg.cmd}`);
} }
} }
@ -180,6 +188,19 @@ async function cameraOpen () {
await camera.open(); await camera.open();
} }
async function cameraClose () {
await camera.close();
}
async function select (id : string) {
const sequencesArr : SequenceObject[] = await Files.enumerateSequences(sequences);
const seq : SequenceObject = sequencesArr.find(el => el.hash === id);
if (typeof seq == 'undefined' || seq == null) {
log.error('Sequence not found, maybe deleted?', new Error(`Cannot find sequence ${id}`));
}
await sequence.load(seq);
}
app.get('/', async (req : Request, res : Response, next : NextFunction) => { app.get('/', async (req : Request, res : Response, next : NextFunction) => {
const sequencesArr : SequenceObject[] = await Files.enumerateSequences(sequences); const sequencesArr : SequenceObject[] = await Files.enumerateSequences(sequences);
//const videosArr : VideoObject[] = await Files.enumerateVideos(videos); //const videosArr : VideoObject[] = await Files.enumerateVideos(videos);
@ -192,8 +213,9 @@ async function main () {
index = await createTemplate('./views/index.hbs'); index = await createTemplate('./views/index.hbs');
ffmpeg = new FFMPEG(process.env['FFMPEG']); ffmpeg = new FFMPEG(process.env['FFMPEG']);
camera = new Camera(); camera = new Camera();
//fd = new FD(process.env['FD'], width, height, process.env['FD_HOST'], parseInt(process.env['FD_PORT'])); //fd = new FD(process.env['FD'], width, height, process.env['FD_HOST'], parseInt(process.env['FD_PORT']));
sequence = new Sequence();
app.listen(port, async () => { app.listen(port, async () => {
log.info(`filmout_manager HTTP server running on port ${port}`); log.info(`filmout_manager HTTP server running on port ${port}`);
}); });

Binary file not shown.

Binary file not shown.

View File

@ -7,4 +7,10 @@ html, body{
#main{ #main{
width: 99vw; width: 99vw;
height: 99vh; height: 99vh;
}
#status {
position: absolute;
bottom: 10px;
width: 97vw;
} }

View File

@ -9,5 +9,10 @@ declare class Client {
private cmd; private cmd;
sendCameraOpen(): void; sendCameraOpen(): void;
private receiveCameraOpen; private receiveCameraOpen;
sendCameraClose(): void;
private receiveCameraClose;
sendSelect(): void;
fullscreen(): void;
exitFullscreen(): void;
} }
declare const client: Client; declare const client: Client;

View File

@ -30,6 +30,9 @@ class Client {
case 'open': case 'open':
this.receiveCameraOpen(); this.receiveCameraOpen();
break; break;
case 'close':
this.receiveCameraClose();
break;
default: default:
console.warn(`No command ${id}`); console.warn(`No command ${id}`);
break; break;
@ -42,6 +45,37 @@ class Client {
receiveCameraOpen() { receiveCameraOpen() {
console.log('got camera open'); console.log('got camera open');
} }
sendCameraClose() {
console.log('send camera close');
this.client.send(JSON.stringify({ cmd: 'close' }));
}
receiveCameraClose() {
console.log('got camera close');
}
sendSelect() {
const sequence = document.getElementById('sequence').value;
let msg;
if (sequence === '- Select Image Sequence -') {
return;
}
msg = { cmd: 'select', sequence };
console.log('send select');
console.log(sequence);
this.client.send(JSON.stringify(msg));
}
fullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
}
else {
this.exitFullscreen();
}
}
exitFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
}
}
} }
const client = new Client(); const client = new Client();
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../browser/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM;IAKV;QAHQ,cAAS,GAAa,KAAK,CAAC;QAIlC,IAAI,GAAG,GAAY,qBAAqB,CAAC;QACzC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAwB,CAAC;QAC3E,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAEO,SAAS,CAAE,KAAW;QAC5B,MAAM,GAAG,GAAa,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;QACxD,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,WAAW,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACpE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACvC,CAAC;aAAM,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,WAAW,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;YAC9D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IAEO,MAAM,CAAE,KAAW;QACzB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACzB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;IAEO,WAAW,CAAC,QAAwB;QAC1C,MAAM,OAAO,GAAY,QAAQ,CAAC,QAAQ,GAAG,KAAK,CAAC;QACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,QAAQ,CAAC,SAAS,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;IACtD,CAAC;IAEO,GAAG,CAAE,EAAW;QACtB,QAAQ,EAAE,EAAE,CAAC;YACX,KAAK,MAAM;gBACT,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACzB,MAAM;YACR;gBACE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBACjC,MAAM;QACV,CAAC;IACH,CAAC;IAEM,cAAc;QACnB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAG,MAAM,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAEO,iBAAiB;QACvB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACjC,CAAC;CACF;AAED,MAAM,MAAM,GAAY,IAAI,MAAM,EAAE,CAAC"} {"version":3,"file":"index.js","sourceRoot":"","sources":["../../browser/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM;IAKV;QAHQ,cAAS,GAAa,KAAK,CAAC;QAIlC,IAAI,GAAG,GAAY,qBAAqB,CAAC;QACzC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAwB,CAAC;QAC3E,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAEO,SAAS,CAAE,KAAW;QAC5B,MAAM,GAAG,GAAa,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;QACxD,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,WAAW,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACpE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACvC,CAAC;aAAM,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,WAAW,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;YAC9D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IAEO,MAAM,CAAE,KAAW;QACzB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACzB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;IAEO,WAAW,CAAC,QAAwB;QAC1C,MAAM,OAAO,GAAY,QAAQ,CAAC,QAAQ,GAAG,KAAK,CAAC;QACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,QAAQ,CAAC,SAAS,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;IACtD,CAAC;IAEO,GAAG,CAAE,EAAW;QACtB,QAAQ,EAAE,EAAE,CAAC;YACX,KAAK,MAAM;gBACT,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACzB,MAAM;YACR,KAAK,OAAO;gBACV,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAC1B,MAAM;YACR;gBACE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBACjC,MAAM;QACV,CAAC;IACH,CAAC;IAEM,cAAc;QACnB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAG,MAAM,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAEO,iBAAiB;QACvB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACjC,CAAC;IAEM,eAAe;QACpB,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAG,OAAO,EAAE,CAAC,CAAC,CAAC;IACtD,CAAC;IAEO,kBAAkB;QACxB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAClC,CAAC;IAEM,UAAU;QACf,MAAM,QAAQ,GAAa,QAAQ,CAAC,cAAc,CAAC,UAAU,CAAwB,CAAC,KAAK,CAAC;QAC5F,IAAI,GAAa,CAAC;QAClB,IAAI,QAAQ,KAAK,2BAA2B,EAAE,CAAC;YAC7C,OAAO;QACT,CAAC;QACD,GAAG,GAAG,EAAE,GAAG,EAAG,QAAQ,EAAE,QAAQ,EAAE,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACxC,CAAC;IAEM,UAAU;QACf,IAAI,CAAC,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAChC,QAAQ,CAAC,eAAe,CAAC,iBAAiB,EAAE,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAEM,cAAc;QACnB,IAAI,QAAQ,CAAC,iBAAiB,EAAE,CAAC;YAC/B,QAAQ,CAAC,cAAc,EAAE,CAAA;QAC3B,CAAC;IACH,CAAC;CAGF;AAED,MAAM,MAAM,GAAY,IAAI,MAAM,EAAE,CAAC"}

View File

@ -7,34 +7,54 @@
</head> </head>
<body> <body>
<div class="window" id="main"> <div class="window" id="main">
<div class="title-bar"> <div class="title-bar">
<div class="title-bar-text"> <div class="title-bar-text">
Filmout Manager Filmout Manager
</div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize" onclick="client.fullscreen();"></button>
<button aria-label="Close"></button>
</div>
</div>
<div class="window-body">
<div>Screen Resolution :
<div class="field-row">
<label for="width">Width</label>
<input id="width" type="text" value="{{width}}" readonly />
</div>
<div class="field-row">
<label for="height">Height</label>
<input id="height" type="text" value="{{height}}" readonly />
</div>
</div>
<!--
<select name="video" id="video">
<option> - Select Video - </option>
{{#each videos}}
<option value="{{this.hash}}">{{this.name}}</option>
{{/each}}
</select>
-->
<div>
<select name="sequence" id="sequence">
<option> - Select Image Sequence - </option>
{{#each sequences}}
<option value="{{this.hash}}">{{this.name}}</option>
{{/each}}
</select>
<button id="select" onclick="client.sendSelect();">Select</button>
</div>
<div>
<button id="open" onclick="client.sendCameraOpen()">Open Gate</button>
<button id="close" onclick="client.sendCameraClose()">Close Gate</button>
</div>
<div class="status-bar" id="status">
<p class="status-bar-field">Idle</p>
<p class="status-bar-field">Progress: 0%</p>
<p class="status-bar-field">Sequence Length: 0</p>
</div>
</div> </div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize"></button>
<button aria-label="Close"></button>
</div>
</div>
<div class="window-body">
<div>Screen Resolution : {{width}}x{{height}}</div>
<!--
<select name="video" id="video">
<option> - Select Video - </option>
{{#each videos}}
<option value="{{this.hash}}">{{this.name}}</option>
{{/each}}
</select>
-->
<select name="sequence" id="sequence">
<option> - Select Image Sequence - </option>
{{#each sequences}}
<option value="{{this.hash}}">{{this.name}}</option>
{{/each}}
</select>
<button id="open" onclick="client.cameraOpen()">Open Gate</button>
</div>
</div> </div>
<script src="/static/js/index.js"></script> <script src="/static/js/index.js"></script>
</body> </body>