10 Commit-ok 48e9cdb2ce ... b37740c9c1

Szerző SHA1 Üzenet Dátum
  Parad0x b37740c9c1 Fixed oauth window 5 éve
  Parad0x b735526d1a - fixed html file path 5 éve
  Parad0x 0a358bfe6d - Added http server 5 éve
  Parad0x 25f5722898 - semi fix for sourcemaps 5 éve
  Parad0x 432daf39f6 - Added client amount to check if obs attached 5 éve
  Parad0x 7d02f74e0c - Changed oauth token field to password field 5 éve
  Parad0x 2bc01b54c1 - Fixed auth header and fixed event type 5 éve
  Parad0x fc23dc7b8f - v0.3.0 nearly final version! 5 éve
  Parad0x da15ed5179 - Moved index.html 5 éve
  Parad0x e4c99e7747 - Added saving status of settings collapse 5 éve

+ 1 - 1
index.html → app/obs/index.html

@@ -85,7 +85,7 @@
             }
         }
 
-        let ws = new WebSocket("ws://localhost:8045");
+        let ws = new WebSocket("ws://localhost:<$wsport$>");
         ws.onmessage = (msg)=>{
             let data = msg.data;
             let commandArray = JSON.parse(msg.data)

+ 1 - 1
app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "twich-points-alerts",
-  "version": "0.2.0",
+  "version": "0.4.0",
   "description": "Simple solution to play mp4 on stream triggered by twitch points",
   "main": "dst/main.js",
   "build": {

+ 8 - 0
app/src/OAuthWindow.ts

@@ -24,7 +24,15 @@ function formatUrl(
 }
 
 function parseTwitchRedirect(twitchUrl: string): string{
+    if(!twitchUrl.startsWith("http://localhost")){
+        return "";
+    }
+    
     let parsed = url.parse(twitchUrl);
+
+    if(!parsed.hash){
+        return "";
+    }
     let query = querystring.parse(parsed.hash.slice(1));
     if(isString(query["access_token"])){
         return query["access_token"];

+ 3 - 1
app/src/main.ts

@@ -20,7 +20,9 @@ function createWindow() {
   });
   mainWindow.setMenu(null);
 
-  mainWindow.webContents.openDevTools();
+  if(process.env.DEVTOOLS == "true"){
+    mainWindow.webContents.openDevTools();
+  }
   mainWindow.loadFile(path.join(__dirname, "../web/index.html"));
   mainWindow.on("closed", () => {
     mainWindow = null;

+ 1 - 0
app/src/web/AlertQueue.ts

@@ -4,6 +4,7 @@ import { EventEmitter } from "events";
 export type AlertElement = {
     filepath: string,
     id: string,
+    alertid: string,
     rewardEvent: PointsRedeemed,
 }
 

+ 182 - 0
app/src/web/HTTPServer.ts

@@ -0,0 +1,182 @@
+import http from "http"
+import {URL} from "url"
+import fs from "fs"
+import path from "path"
+
+function parseAccept(head: string): string[]{
+    let clientAccept = head.split(";");
+    if(!(0 in clientAccept)){
+        this.res.writeHead(400);
+        this.res.end("Problem with accept header");
+        return;
+    }
+    let acc = []
+    for(let it of clientAccept){
+        acc = acc.concat(acc, it.split(","))
+    }
+
+    return acc;
+}
+
+const playerpage: string = fs.readFileSync(
+    path.join(__dirname, "../../obs/index.html"), "utf8");
+
+export class HTTPRequest{
+    private req: http.IncomingMessage;
+    private res: http.ServerResponse;
+
+    private url: URL;
+
+    constructor(req: http.IncomingMessage,res: http.ServerResponse){
+        this.req = req;
+        this.res = res;
+
+        this.url = new URL(req.url, `http://${req.headers.host}`);
+    }
+
+    notFound(){
+        this.res.writeHead(404);
+        this.res.end("Not Found");
+    }
+
+    internalError(){
+        this.res.writeHead(500);
+        this.res.end("Internal Server Error");
+    }
+
+    getPlayerIndex(){
+        this.res.writeHead(200);
+        this.res.end(playerpage.replace("<$wsport$>", 
+          window.settings.options.wsport.toString()));
+    }
+
+    async getMP4(){
+        let id = this.url.pathname.slice(5);
+
+        console.log(this.req.headers);
+
+        if(this.req.headers.accept){
+            let formats = parseAccept(this.req.headers.accept);
+            console.log(formats);
+            if(!(formats.includes("video/*") 
+                || formats.includes("video/mp4")
+                || formats.includes("*/*"))){
+                this.res.writeHead(400);
+                this.res.end("mp4 not supported");
+                return;
+            }
+        }
+
+        if(id in window.settings.options.alerts){
+            console.log(id);
+            let path = window.settings.options.alerts[id].filepath;
+            try {
+                let stat = await fs.promises.stat(path);
+                if(!stat.isFile()){
+                    throw new Error("This is not a file");
+                }
+
+                if(this.req.headers.range &&
+                     this.req.headers.range.startsWith("bytes=") ){
+                    let pos = 
+                      this.req.headers.range.slice(6).split("-");
+                    let start = parseInt(pos[0], 10);
+                    let total = stat.size;
+                    let end = pos[1] ? parseInt(pos[1], 10) : total - 1;
+
+                    this.res.setHeader("Accept-Ranges", "bytes");
+                    this.res.setHeader("Content-Type", "video/mp4");
+                    this.res.setHeader("Content-Range",
+                      `bytes ${start}-${end}/${total}`);
+
+
+                      let stream = fs.createReadStream(path, {
+                          start,
+                          end
+                      });
+                      this.res.writeHead(206);
+                      stream.pipe(this.res);
+                      return
+                }else{
+                    let stream = fs.createReadStream(path);
+                    this.res.setHeader("Content-Type", "video/mp4");
+                    this.res.writeHead(200);
+                    stream.pipe(this.res);
+                    return;
+                }
+
+            } catch(err){
+                this.internalError();
+                return
+            }
+            
+        }else{
+            this.notFound();
+            return
+        }
+    }
+
+    getHandler(){
+        if(this.url.pathname.startsWith("/mp4/")){
+            return this.getMP4();
+        }else if(this.url.pathname == "/"){
+            return this.getPlayerIndex();
+        }else{
+            return this.notFound();
+        }
+    }
+
+    parse(){
+        if(this.req.method == "GET"){
+            return this.getHandler();
+        }else{
+            return this.notFound();
+        }
+    }
+
+}
+
+export class HTTPServer {
+    private port: number;
+    private server?: http.Server;
+
+    private closing: boolean;
+
+    constructor(){
+        this.port = window.settings.options.httpport || 8050;
+        this.closing = false;
+    }
+
+    setPort(port: number){
+        if(this.server && this.server.listening){
+            if(!this.closing){
+                this.closing = true;
+                this.server.close(()=>{
+                    this.port = port;
+                    this.closing = false;
+                    this.listen();
+                })
+            }
+        }else{
+            this.port = port;
+        }
+    }
+
+    create(){
+        this.server = http.createServer((req, res) =>{
+            let request = new HTTPRequest(req, res);
+            request.parse();
+        });
+    }
+
+    listen(){
+        if(this.server){
+            this.server.listen(this.port)
+        }else{
+            throw new Error("Listen before create")
+        }
+    }
+
+}
+
+export default HTTPServer;

+ 39 - 19
app/src/web/Settings.ts

@@ -2,7 +2,6 @@ import { writeFileSync, readFileSync, existsSync, fstat, copyFileSync } from "fs
 import { resolve } from "path";
 import { isArray } from "util";
 import electron, { ipcMain, ipcRenderer } from "electron";
-import { throws } from "assert";
 
 function isSameKeys(o: Object, keys: Set<string>){
     let objectkeys = Object.keys(o);
@@ -27,7 +26,8 @@ const AlertKeys: Set<string> = new Set([
 export type SettingsData = {
     twitch_client_id: string,
     twitch_oauth_token: string,
-    port: number,
+    wsport: number,
+    httpport: number,
     channel: string,
     alerts: {
         [key: string]: Alert
@@ -35,7 +35,8 @@ export type SettingsData = {
 }
 
 const keys: Set<string> = new Set([
-    "port",
+    "wsport",
+    "httpport",
     "twitch_client_id",
     "twitch_oauth_token",
     "channel",
@@ -46,7 +47,8 @@ class Settings {
     options: SettingsData = {
         twitch_client_id: "",
         twitch_oauth_token: "",
-        port: 8045,
+        wsport: 8045,
+        httpport: 8050,
         channel: "",
         alerts: {}
     };
@@ -55,7 +57,8 @@ class Settings {
     private inputs: {
         ClientID?:HTMLInputElement,
         OAuthToken?:HTMLInputElement,
-        Port?:HTMLInputElement,
+        WSPort?:HTMLInputElement,
+        HTTPPort?:HTMLInputElement,
         Channel?:HTMLInputElement,
         SaveButton?:HTMLButtonElement,
         AlertButton?:HTMLButtonElement
@@ -124,14 +127,16 @@ class Settings {
     bind(
         ClientID:HTMLInputElement,
         OAuthToken:HTMLInputElement,
-        Port:HTMLInputElement,
+        WSPort:HTMLInputElement,
+        HTTPPort:HTMLInputElement,
         Channel:HTMLInputElement,
         SaveButton:HTMLButtonElement,
         AlertButton:HTMLButtonElement
     ){
         this.inputs.ClientID = ClientID;
         this.inputs.OAuthToken = OAuthToken;
-        this.inputs.Port = Port;
+        this.inputs.WSPort = WSPort;
+        this.inputs.HTTPPort = HTTPPort;
         this.inputs.Channel = Channel;
         this.inputs.SaveButton = SaveButton;
         this.inputs.AlertButton = AlertButton;
@@ -143,14 +148,28 @@ class Settings {
             this.options.twitch_client_id = this.inputs.ClientID.value;
             this.options.channel = this.inputs.Channel.value;
     
-            // Port
-            let port: number = parseInt(this.inputs.Port.value);
-            if(!isNaN(port) && port > 0 && port < 65535){
-                this.options.port = port;
-                this.inputs.Port.value = port.toString();
+            // WSPort
+            let wsport: number = parseInt(this.inputs.WSPort.value);
+            if(!isNaN(wsport) && wsport > 0 && wsport < 65535){
+                this.options.wsport = wsport;
+                this.inputs.WSPort.value = wsport.toString();
             }else{
-                console.log("Wrong value for port");
-                this.inputs.Port.value = this.options.port.toString();
+                console.log("Wrong value for wsport");
+                this.inputs.WSPort.value = this.options.wsport.toString();
+                // TODO: Some error box
+            }
+
+            // HTTP Port
+            let httpport: number = parseInt(this.inputs.HTTPPort.value);
+            if(!isNaN(httpport) && httpport > 0 && httpport < 65535){
+                if(this.options.httpport != httpport){
+                    window.httpserver.setPort(httpport);
+                }
+                this.options.httpport = httpport;
+                this.inputs.HTTPPort.value = httpport.toString();
+            }else{
+                console.log("Wrong value for httpport");
+                this.inputs.HTTPPort.value = this.options.httpport.toString();
                 // TODO: Some error box
             }
     
@@ -168,13 +187,13 @@ class Settings {
             this.options.channel = this.inputs.Channel.value;
       
             // Port
-            let port: number = parseInt(this.inputs.Port.value);
+            let port: number = parseInt(this.inputs.WSPort.value);
             if(!isNaN(port) && port > 0 && port < 65535){
-                this.options.port = port;
-                this.inputs.Port.value = port.toString();
+                this.options.wsport = port;
+                this.inputs.WSPort.value = port.toString();
             }else{
                 console.log("Wrong value for port");
-                this.inputs.Port.value = this.options.port.toString();
+                this.inputs.WSPort.value = this.options.wsport.toString();
                 // TODO: Some error box
             }
       
@@ -189,7 +208,8 @@ class Settings {
 
     updateView(){
         this.inputs.ClientID.value = this.options.twitch_client_id
-        this.inputs.Port.value = this.options.port.toString()
+        this.inputs.WSPort.value = this.options.wsport.toString()
+        this.inputs.HTTPPort.value = this.options.httpport.toString()
         this.inputs.OAuthToken.value = this.options.twitch_oauth_token;
         this.inputs.Channel.value = this.options.channel;
     }

+ 4 - 1
app/src/web/TwitchPubSug.ts

@@ -46,8 +46,11 @@ export interface MesageEvent {
         topic: string
     }
 }
+export interface PongEvent {
+    type: "PONG"
+}
 
-export type TwitchEvent = MesageEvent & ResponseEvent
+export type TwitchEvent = MesageEvent | ResponseEvent | PongEvent
 
 declare interface Twitch {
     on(event: "reward", listner: (reward: PointsRedeemed)=>void): this;

+ 20 - 6
app/src/web/WSServer.ts

@@ -24,7 +24,8 @@ class WebSocketServer extends EventEmitter {
 
     private interval?: NodeJS.Timeout;
     private inputs: {
-        StartStopButton?: HTMLButtonElement
+        StartStopButton?: HTMLButtonElement,
+        ClientsAmountField?: HTMLSpanElement
     } = {};
 
     constructor(private port: number = 8045){
@@ -50,9 +51,10 @@ class WebSocketServer extends EventEmitter {
             console.log("Trying to start websocket when it is already running");
             return
         }
-        this.changePort(window.settings.options.port);
+        this.changePort(window.settings.options.wsport);
         this.server = new ws.Server({
-            port: this.port
+            port: this.port,
+            clientTracking: true
         });
 
         this.server.on("close", ()=>{
@@ -96,13 +98,22 @@ class WebSocketServer extends EventEmitter {
         
     }
 
+    clientsAmount(): number{
+        if(this.server){
+            return this.server.clients.size;
+        }
+        return 0;
+    }
+
     bind(
-        StaStoBtn: HTMLButtonElement
+        StaStoBtn: HTMLButtonElement,
+        ClientsAmountField: HTMLSpanElement
     ){
         this.inputs.StartStopButton = StaStoBtn;
+        this.inputs.ClientsAmountField = ClientsAmountField;
 
         let refresher = () => this.updateElement();
-        this.interval = setInterval(refresher, 1000);
+        this.interval = setInterval(refresher, 333);
         setTimeout(refresher, 0);
 
         this.inputs.StartStopButton.addEventListener("click", e => {
@@ -110,7 +121,7 @@ class WebSocketServer extends EventEmitter {
           if(this.status()){
             this.stop();
           }else{
-            this.changePort(window.settings.options.port)
+            this.changePort(window.settings.options.wsport)
             this.start();
           }
         })
@@ -129,6 +140,9 @@ class WebSocketServer extends EventEmitter {
             if(this.inputs.StartStopButton)
                 this.inputs.StartStopButton.innerText = "Start";
         }
+
+        this.inputs.ClientsAmountField.textContent = 
+            this.clientsAmount().toString();
     }
 
     sendToAll(msg: string){

+ 3 - 1
app/src/web/global.d.ts

@@ -1,8 +1,10 @@
 import fs from "fs"
 import Settings from "./Settings"
+import HTTPServer from "./HTTPServer"
 declare global {
     interface Window {
         fs: typeof fs,
-        settings: Settings
+        settings: Settings,
+        httpserver: HTTPServer
     }
 }

+ 12 - 4
app/src/web/preload.ts

@@ -5,6 +5,8 @@ import WebSocketServer from "./WSServer";
 import AlertFront from "./AlertFront";
 import Queue, { AlertElement } from "./AlertQueue";
 import generateID from "./utils/generateid";
+import HTTPServer from "./HTTPServer";
+import { KeyObject } from "crypto";
 
 let settings = window.settings = new Settings()
 
@@ -13,6 +15,9 @@ const wss = new WebSocketServer();
 
 wss.start();
 
+const httpserver = window.httpserver =  new HTTPServer();
+httpserver.create();
+httpserver.listen();
 
 const AlertQueue = new Queue<AlertElement>();
 
@@ -31,13 +36,13 @@ function sendPath(path: string){
 wss.on("waitingForVideo", ()=>{
   if(AlertQueue.len() > 0){
     let el = AlertQueue.remove();
-    sendPath(el.filepath);
+    sendPath(`/mp4/${el.alertid}`);
   }
 })
 wss.on("ended", ()=>{
   if(AlertQueue.len() > 0){
     let el = AlertQueue.remove();
-    sendPath(el.filepath);
+    sendPath(`/mp4/${el.alertid}`);
   }
 })
 
@@ -64,7 +69,8 @@ window.addEventListener("DOMContentLoaded", () => {
   settings.bind(
     document.getElementById("settClientToken") as HTMLInputElement,
     document.getElementById("settOAuthToken") as HTMLInputElement,
-    document.getElementById("settPort") as HTMLInputElement,
+    document.getElementById("settWSPort") as HTMLInputElement,
+    document.getElementById("settHTTPPort") as HTMLInputElement,
     document.getElementById("settChannel") as HTMLInputElement,
     document.getElementById("settSave") as HTMLButtonElement,
     document.getElementById("saveAlert") as HTMLButtonElement
@@ -84,7 +90,8 @@ window.addEventListener("DOMContentLoaded", () => {
   );
 
   wss.bind(
-    document.getElementById("wssstartstop") as HTMLButtonElement
+    document.getElementById("wssstartstop") as HTMLButtonElement,
+    document.getElementById("clientsAmountNumber") as HTMLSpanElement
   )
 
   let getTokenButton = document.getElementById("settGetToken");
@@ -108,6 +115,7 @@ window.addEventListener("DOMContentLoaded", () => {
           let newid = generateID(12);
           AlertQueue.add({
             filepath: al[key].filepath,
+            alertid: key,
             id: newid,
             rewardEvent: reward
           })

+ 1 - 1
app/src/web/utils/translateTwitchUser.ts

@@ -7,7 +7,7 @@ export default async function translateTwitchUser(user: string): Promise<string>
         method: "GET",
         headers: {
             "Client-ID": window.settings.options.twitch_client_id,
-            "Authorization": "OAuth "+window.settings.options.twitch_oauth_token
+            "Authorization": "Bearer "+window.settings.options.twitch_oauth_token
         }
     });
     let userData = await response.json();

+ 5 - 5
app/tsconfig.json

@@ -10,7 +10,7 @@
     // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
     // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
     // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
-    "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
     // "outFile": "./",                       /* Concatenate and emit output to single file. */
     "outDir": "./dst",                        /* Redirect output structure to the directory. */
     "rootDir": "./src",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
@@ -51,10 +51,10 @@
     // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
 
     /* Source Map Options */
-    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
-    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
-    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
-    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+    // "sourceRoot": "../src",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "../app/dst",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+    "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+    "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
 
     /* Experimental Options */
     // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */

+ 56 - 11
app/web/index.html

@@ -23,13 +23,19 @@
 
         #settOAuthToken {
             display: inline-block; 
-            width: calc( 100% - 105px )
+            width: calc( 100% - 170px )
         }
         #settGetToken {
             width: 100px;
             float: right;
         }
 
+        #settShowToken {
+            width: 60px;
+            float: right;
+            margin-right: 5px;
+        }
+
         #alerts > form {
             margin-bottom: 1em;
         }
@@ -60,7 +66,7 @@
             <h3>Settings</h3>
             <a id="hideButton" data-toggle="collapse" data-target="#sett" style="padding: 5px">
             </a>
-            <form id="sett" class="col-12 show">
+            <form id="sett" class="col-12 collapse">
                 <div class="form-row">
                     <div class="form-group col-12">
                         <label for="settClientToken">Twitch Client ID</label>
@@ -70,16 +76,21 @@
                         <label for="settOAuthToken" style="display: block;">Twitch OAuth Token</label>
                     </div>
                     <div class="form-group col-12 align-items-center">
-                        <input type="text" class="form-control" id="settOAuthToken" readonly>
+                        <input type="password" class="form-control" id="settOAuthToken" readonly>
                         <button class="btn btn-primary" id="settGetToken">Get token</button>
+                        <button class="btn btn-danger" id="settShowToken">Show</button>
                     </div>
                     <div class="form-group col-12">
                         <label for="settChannel">Channel</label>
                         <input type="text" class="form-control" id="settChannel">
                     </div>
                     <div class="form-group col-12">
-                        <label for="settPort">Port</label>
-                        <input type="text" class="form-control" id="settPort">
+                        <label for="settWSPort">WS Port</label>
+                        <input type="text" class="form-control" id="settWSPort">
+                    </div>
+                    <div class="form-group col-12">
+                        <label for="settHTTPPort">HTTP Port</label>
+                        <input type="text" class="form-control" id="settHTTPPort">
                     </div>
                 </div>
                 <div class="form-group">
@@ -92,7 +103,7 @@
                 Twitch Connection: <span id="twitchStatus"></span> <button id="twitchstartstop" class="btn btn-primary">Start/Stop</button> <span id="twitchError" class="error-mesage"></span>
             </p>
             <p>
-                WebSocket Server Connection: <span id="wssStatus"></span> <button id="wssstartstop" class="btn btn-primary">Start/Stop</button>
+                WebSocket Server Connection: <span id="wssStatus"></span> <button id="wssstartstop" class="btn btn-primary">Start/Stop</button> <span id="clientsAmount">Clients: <span id="clientsAmountNumber">test2</span></span>
             </p>
         </div>
         <h3>Alerts</h3>
@@ -104,27 +115,61 @@
         <button id="abortAlert" class="btn btn-danger">Abort</button>
         <h3>Queue length: <span id="queueCounter"></span></h3>
     </div>
-    <script src="lib/jquery-3.4.1.slim.min.js"></script>
-    <script src="lib/popper.min.js"></script>
-    <script src="lib/bootstrap.min.js"></script>
     <script src="lib/feather.min.js"></script>
-    <script src="../dst/web/main.js"></script>
+    <script src="lib/jquery-3.4.1.slim.min.js"></script>
     <script>
         let col2 = $("#sett");
+        col2.submit(e => {
+            e.preventDefault();
+        })
+        let isSettingsHidden = localStorage.getItem("isSettingsHidden") == "true";
         let button = document.getElementById("hideButton");
         let svgOptions = {
             class: "align-middle"
         };
-        button.innerHTML = feather.icons['chevron-up'].toSvg(svgOptions);
+
+        // Showing token
+        let oauthFormInput = document.getElementById("settOAuthToken");
+        let showButton = document.getElementById("settShowToken");
+        showButton.addEventListener("click", ()=>{
+            return false;
+        })
+        showButton.addEventListener("mousedown", ()=>{
+            oauthFormInput.setAttribute("type", "text");
+            return false;
+        })
+        showButton.addEventListener("mouseup", ()=>{
+            oauthFormInput.setAttribute("type", "password");
+            return false;
+        })
+        showButton.addEventListener("mouseleave", ()=>{
+            oauthFormInput.setAttribute("type", "password");
+            return false;
+        })
+
+        if(!isSettingsHidden){
+            col2.addClass("show");
+        }
+
+        if(isSettingsHidden){
+            button.innerHTML = feather.icons['chevron-down'].toSvg(svgOptions);
+        }else{
+            button.innerHTML = feather.icons['chevron-up'].toSvg(svgOptions);
+        }
         col2.on("show.bs.collapse", ()=>{
             console.log("Show 2");
+            localStorage.setItem("isSettingsHidden", "false");
             button.innerHTML = feather.icons['chevron-up'].toSvg(svgOptions);
         })
         col2.on("hide.bs.collapse", ()=>{
             console.log("Hide 2");
+            localStorage.setItem("isSettingsHidden", "true");
             button.innerHTML = feather.icons['chevron-down'].toSvg(svgOptions);
         })
         feather.replace()
     </script>
+    <script src="lib/popper.min.js"></script>
+    <script src="lib/bootstrap.min.js"></script>
+    <!-- <script src="../dst/web/main.js"></script> -->
 </body>
 </html>