master c4d7be999ac7 cached
68 files
1.5 MB
425.2k tokens
1326 symbols
1 requests
Download .txt
Showing preview only (1,612K chars total). Download the full file or copy to clipboard to get everything.
Repository: imTigger/webapp-hardware-bridge
Branch: master
Commit: c4d7be999ac7
Files: 68
Total size: 1.5 MB

Directory structure:
gitextract_p8znxfti/

├── .gitignore
├── .idea/
│   ├── artifacts/
│   │   └── webapp_hardware_bridge_jar.xml
│   ├── compiler.xml
│   ├── gradle.xml
│   ├── inspectionProfiles/
│   │   └── Project_Default.xml
│   ├── jarRepositories.xml
│   ├── misc.xml
│   ├── modules/
│   │   ├── webapp-hardware-bridge.iml
│   │   └── webapp-hardware-bridge.main.iml
│   ├── modules.xml
│   └── vcs.xml
├── ADVANCED.md
├── BUILD.md
├── CHANGELOG.md
├── CONFIGURATION.md
├── HTTP_API.md
├── LICENSE
├── README.md
├── TROUBLESHOOT.md
├── build.gradle
├── demo/
│   ├── printer-advanced.htm
│   ├── printer-annotation.htm
│   ├── printer-basic.htm
│   ├── serial-basic.html
│   ├── serial-weigh.htm
│   ├── websocket-printer.js
│   ├── websocket-serial.js
│   └── websocket-weigh.js
├── gradlew
├── gradlew.bat
├── install.nsi
├── settings.gradle
└── src/
    └── main/
        ├── java/
        │   ├── module-info.java
        │   └── tigerworkshop/
        │       └── webapphardwarebridge/
        │           ├── Constants.java
        │           ├── GUI.java
        │           ├── Server.java
        │           ├── dtos/
        │           │   ├── Config.java
        │           │   ├── NotificationDTO.java
        │           │   ├── PrintServiceDTO.java
        │           │   ├── SerialPortDTO.java
        │           │   └── VersionDTO.java
        │           ├── interfaces/
        │           │   ├── WebSocketServerInterface.java
        │           │   └── WebSocketServiceInterface.java
        │           ├── responses/
        │           │   ├── PrintDocument.java
        │           │   └── PrintResult.java
        │           ├── services/
        │           │   ├── ConfigService.java
        │           │   └── DocumentService.java
        │           ├── utils/
        │           │   ├── AnnotatedPrintable.java
        │           │   ├── CertificateGenerator.java
        │           │   ├── ImagePrintable.java
        │           │   └── ThreadUtil.java
        │           └── websocketservices/
        │               ├── PrinterWebSocketService.java
        │               └── SerialWebSocketService.java
        └── resources/
            ├── META-INF/
            │   └── MANIFEST.MF
            ├── log4j2.xml
            └── web/
                ├── css/
                │   ├── bootstrap-grid.css
                │   ├── bootstrap-grid.rtl.css
                │   ├── bootstrap-reboot.css
                │   ├── bootstrap-reboot.rtl.css
                │   ├── bootstrap-utilities.css
                │   ├── bootstrap-utilities.rtl.css
                │   ├── bootstrap.css
                │   └── bootstrap.rtl.css
                ├── index.html
                └── js/
                    ├── bootstrap.bundle.js
                    ├── bootstrap.esm.js
                    ├── bootstrap.js
                    └── petite-vue.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
downloads/
.gradle/
gradle/
log/
out/
jre/
build/
.idea/workspace.xml
config.json
*.exe

================================================
FILE: .idea/artifacts/webapp_hardware_bridge_jar.xml
================================================
<component name="ArtifactManager">
  <artifact name="webapp-hardware-bridge:jar">
    <output-path>$PROJECT_DIR$/out/artifacts/webapp_hardware_bridge_jar</output-path>
    <root id="root">
      <element id="archive" name="webapp-hardware-bridge.jar">
        <element id="directory" name="META-INF">
          <element id="file-copy" path="$PROJECT_DIR$/src/main/resources/META-INF/MANIFEST.MF" />
        </element>
        <element id="directory" name="web">
          <element id="dir-copy" path="$PROJECT_DIR$/src/main/resources/web" />
        </element>
        <element id="module-output" name="webapp-hardware-bridge.main" />
        <element id="file-copy" path="$PROJECT_DIR$/src/main/resources/icon.png" />
        <element id="file-copy" path="$PROJECT_DIR$/src/main/resources/log4j2.xml" />
      </element>
      <element id="library" level="project" name="Gradle: com.fasterxml.jackson.core:jackson-annotations:2.17.2" />
      <element id="library" level="project" name="Gradle: com.fasterxml.jackson.core:jackson-core:2.17.2" />
      <element id="library" level="project" name="Gradle: com.fasterxml.jackson.core:jackson-databind:2.17.2" />
      <element id="library" level="project" name="Gradle: com.fazecast:jSerialComm:2.11.0" />
      <element id="library" level="project" name="Gradle: commons-codec:commons-codec:1.17.1" />
      <element id="library" level="project" name="Gradle: commons-io:commons-io:2.16.1" />
      <element id="library" level="project" name="Gradle: commons-logging:commons-logging:1.2" />
      <element id="library" level="project" name="Gradle: io.github.hakky54:sslcontext-kickstart-for-jetty:8.3.6" />
      <element id="library" level="project" name="Gradle: io.github.hakky54:sslcontext-kickstart-for-pem:8.3.6" />
      <element id="library" level="project" name="Gradle: io.github.hakky54:sslcontext-kickstart:8.3.6" />
      <element id="library" level="project" name="Gradle: io.javalin.community.ssl:ssl-plugin:6.2.0" />
      <element id="library" level="project" name="Gradle: io.javalin:javalin:6.2.0" />
      <element id="library" level="project" name="Gradle: junit:junit:4.13.2" />
      <element id="library" level="project" name="Gradle: org.apache.httpcomponents.core5:httpcore5:5.2.5" />
      <element id="library" level="project" name="Gradle: org.apache.logging.log4j:log4j-api:2.23.1" />
      <element id="library" level="project" name="Gradle: org.apache.logging.log4j:log4j-core:2.23.1" />
      <element id="library" level="project" name="Gradle: org.apache.logging.log4j:log4j-slf4j2-impl:2.23.1" />
      <element id="library" level="project" name="Gradle: org.apache.pdfbox:fontbox:2.0.31" />
      <element id="library" level="project" name="Gradle: org.apache.pdfbox:pdfbox:2.0.31" />
      <element id="library" level="project" name="Gradle: org.bouncycastle:bcpkix-jdk18on:1.78.1" />
      <element id="library" level="project" name="Gradle: org.bouncycastle:bcprov-jdk18on:1.78.1" />
      <element id="library" level="project" name="Gradle: org.bouncycastle:bcutil-jdk18on:1.78.1" />
      <element id="library" level="project" name="Gradle: org.conscrypt:conscrypt-openjdk-uber:2.5.2" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.http2:http2-common:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.http2:http2-hpack:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.http2:http2-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api:5.0.2" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.websocket:websocket-core-common:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.websocket:websocket-core-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.websocket:websocket-jetty-api:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.websocket:websocket-jetty-common:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.websocket:websocket-jetty-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty.websocket:websocket-servlet:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-alpn-conscrypt-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-alpn-java-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-alpn-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-http:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-io:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-security:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-server:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-servlet:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-util:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-webapp:11.0.21" />
      <element id="library" level="project" name="Gradle: org.eclipse.jetty:jetty-xml:11.0.21" />
      <element id="library" level="project" name="Gradle: org.hamcrest:hamcrest-core:1.3" />
      <element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.24" />
      <element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24" />
      <element id="library" level="project" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib:1.9.24" />
      <element id="library" level="project" name="Gradle: org.jetbrains:annotations:13.0" />
      <element id="library" level="project" name="Gradle: org.projectlombok:lombok:1.18.34" />
      <element id="library" level="project" name="Gradle: org.slf4j:slf4j-api:2.0.13" />
    </root>
  </artifact>
</component>

================================================
FILE: .idea/compiler.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="CompilerConfiguration">
    <annotationProcessing>
      <profile default="true" name="Default" enabled="true" />
      <profile name="Gradle Imported" enabled="true">
        <outputRelativeToContentRoot value="true" />
        <processorPath useClasspath="false">
          <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.34/ec547ef414ab1d2c040118fb9c1c265ada63af14/lombok-1.18.34.jar" />
        </processorPath>
        <module name="webapp-hardware-bridge.test" />
        <module name="webapp-hardware-bridge.main" />
      </profile>
    </annotationProcessing>
    <bytecodeTargetLevel target="21" />
  </component>
</project>

================================================
FILE: .idea/gradle.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="GradleMigrationSettings" migrationVersion="1" />
  <component name="GradleSettings">
    <option name="linkedExternalProjectsSettings">
      <GradleProjectSettings>
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
        <option name="modules">
          <set>
            <option value="$PROJECT_DIR$" />
          </set>
        </option>
      </GradleProjectSettings>
    </option>
  </component>
</project>

================================================
FILE: .idea/inspectionProfiles/Project_Default.xml
================================================
<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Project Default" />
    <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
      <option name="myValues">
        <value>
          <list size="7">
            <item index="0" class="java.lang.String" itemvalue="@vue:mounted" />
            <item index="1" class="java.lang.String" itemvalue="v-scope" />
            <item index="2" class="java.lang.String" itemvalue="@click" />
            <item index="3" class="java.lang.String" itemvalue="v-if" />
            <item index="4" class="java.lang.String" itemvalue="v-for" />
            <item index="5" class="java.lang.String" itemvalue="v-model" />
            <item index="6" class="java.lang.String" itemvalue=":value" />
          </list>
        </value>
      </option>
      <option name="myCustomValuesEnabled" value="true" />
    </inspection_tool>
  </profile>
</component>

================================================
FILE: .idea/jarRepositories.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="RemoteRepositoriesConfiguration">
    <remote-repository>
      <option name="id" value="central" />
      <option name="name" value="Maven Central repository" />
      <option name="url" value="https://repo1.maven.org/maven2" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="jboss.community" />
      <option name="name" value="JBoss Community repository" />
      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="MavenRepo" />
      <option name="name" value="MavenRepo" />
      <option name="url" value="https://repo.maven.apache.org/maven2/" />
    </remote-repository>
  </component>
</project>

================================================
FILE: .idea/misc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ExternalStorageConfigurationManager" enabled="true" />
  <component name="FrameworkDetectionExcludesConfiguration">
    <file type="web" url="file://$PROJECT_DIR$" />
  </component>
  <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
    <output url="file://$PROJECT_DIR$/out" />
  </component>
</project>

================================================
FILE: .idea/modules/webapp-hardware-bridge.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
  <component name="ExternalSystem" externalSystem="GRADLE" externalSystemModuleGroup="webapp-hardware-bridge" externalSystemModuleVersion="1.0-SNAPSHOT" linkedProjectId="webapp-hardware-bridge" linkedProjectPath="$MODULE_DIR$/../.." rootProjectPath="$MODULE_DIR$/../.." />
</module>

================================================
FILE: .idea/modules/webapp-hardware-bridge.main.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
  <component name="AdditionalModuleElements">
    <content url="file://$MODULE_DIR$/../../build/generated/sources/annotationProcessor/java/main">
      <sourceFolder url="file://$MODULE_DIR$/../../build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
    </content>
  </component>
</module>

================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/modules/webapp-hardware-bridge.main.iml" filepath="$PROJECT_DIR$/.idea/modules/webapp-hardware-bridge.main.iml" />
    </modules>
  </component>
</project>

================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="" vcs="Git" />
  </component>
</project>

================================================
FILE: ADVANCED.md
================================================
# Advanced Configurations

## Authentication

### Enable Authentication

Authentication is disabled by default, that any website can connect to your bridge and access local resources.

To prevent unauthorized access, set `server.authentication.enabled` to `true` and `server.authentication.token` to the value you want.

When enabled, connections without correct token will be rejected.

#### WebSocket

Point url to `ws://127.0.0.1:12212/serial/WEIGH?token=1234567890` if your `token` is "1234567890"

#### Web UI

Enter `token` as `Password` when prompted and leave `Username` empty.

#### Web API

Use header `Authorization: Bearer 1234567890`

## HTTPS/WSS Support

Some browser does not allow webpage with secure context (i.e. HTTPS)
to connect non-secured WebSocket server.

Either of below methods required to workaround this:

### Allow non-secure WebSocket server from HTTPS website (Not recommended)

Warning: These setting can open a security hole, use on development environment only

Firefox: Go to `about:config`, set `network.websocket.allowInsecureFromHTTPS` to `true`

Chrome: Add `--allow-running-insecure-content` to launching argument

### Enable WebSocket Secure (WSS) with self-signed certificate

WHB have built-in ability to generate self-signed certificate.

Set `server.tls.enabled` to true, `server.tls.selfSigned` to true in `setting.json` and relaunch the application.

Upon start, application should automatically generate a self-signed certificate

and start listening on `wss://127.0.0.1:12212` with secured connection.

On first setup, you must go to `https://127.0.0.1:12212` to accept that self-signed certificate.

After change, point url to `wss://127.0.0.1:12212` instead of `ws://127.0.0.1:12212`

### Enable WebSocket Secure (WSS) with real, user-provided certificate

Copy your certificate and private key to `tls` directory.

Set `server.tls.enabled` to true, `server.tls.selfSigned` to false, `server.tls.cert` and `server.tls.key` in `setting.json` and relaunch the application.

Upon start, application should pickup your certificate and start listening on `wss://127.0.0.1:12212` with secured connection.

After change, point url to `wss://127.0.0.1:12212` instead of `ws://127.0.0.1:12212`

#### How to obtain real TLS Certificate?

WHB is usually listening on 127.0.0.1. It's normally impossible to obtain valid certificates signed for that.

A common workaround is to point your (sub-)domain A Record to 127.0.0.1, and obtain certificate with that

e.g. Point `local.tiger-workshop.com` to `127.0.0.1`, then point your WebApp to `wss://local.tiger-workshop.com:12212`

### Why we can't provide certificate for you

Shipping private key with application is considered kind of "key-compromise".

The certificate will be revoked by CA. It's very easily detected especially for open-source projects.

================================================
FILE: BUILD.md
================================================
# Build Instructions

## Build from source

- JDK 21, [Eclipse Temurin 21](https://adoptium.net/en-GB/temurin/releases/) Recommanded
- Intelij IDEA (Both Community and Ultimate works)

1. An artifact config file is included in git repository.

2. Use Intelij IDEA to "Build artifact" to yield `out\artifacts\webapp_hardware_bridge_jar`.

## Windows Installer bundled with JRE

- JRE 21, [Eclipse Temurin 21](https://adoptium.net/en-GB/temurin/releases/) Recommanded
- [Nullsoft Scriptable Install System](https://nsis.sourceforge.io/) 

1. Follow "Build from source" instructions to yield `out\artifacts\webapp_hardware_bridge_jar`

2. Copy JRE 21 into `./jre` directory 

3. Run `install.nsi` with NSIS to yield `whb.exe`

## How to run

1. Start application
   - GUI: `javaw -cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI`
   - Server: `java -cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.Server`

================================================
FILE: CHANGELOG.md
================================================
# Changelogs

## From 0.x to 1.0.0

- 1.0 is a major rewrite, while maintain compatibility with existing WebApps
- Settings will lost after upgrade, please reconfigure via "Web UI" or "Web API"

### Feature changes
- Added per printer settings (Auto-rotate, DPI...)
- Added per serial port settings (Baud-rate, data bits, stop bit, parity bit, charset. binary mode, multi-bytes mode)
- Added "Web UI" for configuration, replacing "Configurator"
- Added "Web API", a HTTP API for WebApp to configure directly without using "Web UI" or "Configurator"
- Config file renamed from "setting.json" to "config.json", which is in different format

### Internal changes
- Removed "Configurator"
- Removed undocumented feature "Cloud Proxy"
- Removed usage of JavaFX
- Rewrite config code
- Implementation of WebSocket changed from "Java-WebSocket" to "Javalin"
- Internal dataflow optimization
- Simplified code by using "Lombok"
- Upgrade Java version from 8 to 21
- Many dependencies upgrades and security fixes


================================================
FILE: CONFIGURATION.md
================================================
# Configurations

## Web/WebSocket Server

### Bind

- (Default) `127.0.0.1` 
- `127.0.0.1` for normal usage
- `0.0.0.0` for open to internet access (Not recommended)
- Other interface address accepted

### Address

- (Default) `127.0.0.1` 
- `127.0.0.1` unless you need to allow internal/internet access
- IP address / Domain name accepted

### Port

- (Default) `12212`
- Range: `1024` - `65535`

### Enable authentication

[See Authentication for more detail](ADVANCED.md#authentication)

- (Default) `false`

#### Token

- (Default) Blank
- Accept any text value

### Enable TLS

[See HTTPS/WSS Support for more detail](ADVANCED.md#httpswss-support)

- (Default) `false`

#### Self Signed

- (Default) `true`

#### Cert

- (Default) `tls/default-cert.pem`

#### Key

- (Default) `tls/default-cert.pem`

#### CA Bundle

- (Default) Empty

## Downloader

### Path

Directory to save downloaded files

- (Default) `download`

### Timeout

Seconds before download timeout

- (Default) `30`

### Ignore TLS certificate error

Ignore any TLS certificate error (self-signed, expired...) when downloading files

Not recommended for normal usage, useful in some corporate networks where firewall doing MITM

- (Default) `false`

## Printers

### Enabled

- (Default) `true`

### Auto add unknown type

Auto add type mapping to configuration when document received with unknown type

- (Default) `false`

### Fallback to default printer if none matched

Fallback to default printer if none of the printers matched in configuration

- (Default) `false`

#### Type

Mapping key between WebApp and physically printer name in operating system

#### Printer Name

Printer name in operating system

#### Auto Rotate

Auto rotate portrait / landscape

- (Default) `false`

#### Reset imageable area

Required by some printer to handle size correctly

- (Default) `true`

#### Force DPI

Required by some printer/operating system to handle DPI correctly

- (Default) `0` 
-  `0` - Auto detect
- Common values: `213`, `300`

## Serials

### Enabled

- (Default) `true`

#### Type

Mapping key between WebApp and physically serial port name in operating system

#### Serial Port

Serial port name in operating system

#### Baud Rate

Auto-detect when leave blank

- (Default) Blank

#### Data Bits

Auto-detect when leave blank

- (Default) Blank

#### Stop Bits

Auto-detect when leave blank

- (Default) Blank

#### Parity

Auto-detect when leave blank

- (Default) Blank

#### Read Charset

Charset to decode data received from serial port

Changing this may break compatibility with WebApp integrated with pre-1.0 version

- (Default) `UTF-8`
- `UTF-8` - Data will be sent to WebSocket as UTF-8 `string`
- `US-ASCII` - Data will be sent to WebSocket as ASCII `string`
- `BINARY` - Data will be sent to WebSocket as `blob`

#### Read Multi-bytes

Read all available bytes in serial port and send them to WebSocket at once

Changing this may break compatibility with WebApp integrated with pre-1.0 version

- (Default) `false`

================================================
FILE: HTTP_API.md
================================================
# HTTP APIs

All endpoints have CORS configured to allow requests from any origin.

You can get or update the current configuration in your WebApp directly by using the `/config.json` endpoint.

## GET /config.json

Get content of `config.json` file.

## PUT /config.json

Update content of `config.json` file.

## GET /system/printers.json

Return list of available printers.

## GET /system/serials.json

Return list of available serial ports.

## POST /system/restart.json

Restart WebSocket/Web server

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 imTigger

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# WebApp Hardware Bridge

## Introduction

WebApp Hardware Bridge made it possible for WebApps to perform silent print and access to serial ports.

Common use cases:
- Web-based POS - PDF and ESC/POS receipt silent print
- Web-based WMS - Serial weight scale real-time reading, delivery note/packing List silent print
- Any WebApps need to read/write to serial ports

## Features

- [x] Direct print from WebApps
- [x] Serial port read/write from WebApps
- [x] Support all modern browsers that implemented WebSocket (Chrome, Firefox, Edge... etc)
- [x] [HTTP API](HTTP_API.md) to configure directly from your WebApp
- [x] [JS SDK/Example included](demo)

### Direct Print
- 0-click silent printing in web browsers
- Download via URL / Base64 encoded file / Base64 encoded binary raw command
- Support multiple printers, mapped by key
- Support PDF/PNG/JPG Printing
- Support RAW/ESC-POS Printing
- Support adding annotation text to PDF/Image before printing
- Per printer settings

### Serial Access
- Bidirectional communication
- Support multiple ports, mapped by key
- Support multiple connection share same serial port
- Serial weigh scale (AWH-SA30 supported out-of-box in JS SDK)
- Per port settings (Baud rate, data bits, stop bit, parity bit)

## How to use?

### Client Side

1. Install and setup mapping via Web UI / API

2. Start "WebApp Hardware Bridge" and start using your WebApp

### WebApp Side

1. Check [JS SDK/Example](demo)

## How it works?

WebApp Hardware Bridge is a Java based application, which have more access to underlying hardwares.

It exposes a WebSocket server on localhost to accept print jobs and serial connections from browsers.

### Print Jobs 

- PDF/Images job are downloaded/decoded and then sent to mapped printer.
- Raw job are sent to mapped printer directly.

### Serial Connections

- Serial port are opened by Java and "proxied" as WebSocket stream
- Serial port can be shared by multiple connections
- Bidirectional communications possible

### Mappings

Web UI / API are provided to set up mappings between keys and printers/serials.

Therefore, WebApps do not need to care about the actual printer names.

## More documents

- [Configurations](CONFIGURATION.md)
- [HTTP APIs](HTTP_API.md)
- [Advanced Configurations - Authentication](ADVANCED.md#authentication)
- [Advanced Configurations - HTTPS/WSS Support](ADVANCED.md#httpswss-support)
- [Build from source](BUILD.md)
- [Troubleshooting](TROUBLESHOOT.md)

## Upgrade

- Settings will lost after upgrade from 0.x to 1.0, please reconfigure via "Web UI" or "Web API"

## Changelogs

- [Changelogs](CHANGELOG.md)


================================================
FILE: TROUBLESHOOT.md
================================================
# Troubleshoot

- Configurator/GUI do not run? Install [vc_redist.x64.exe](https://www.microsoft.com/en-US/download/details.aspx?id=48145)

================================================
FILE: build.gradle
================================================
plugins {
    id 'java'
    id 'application'
}

group 'webapp-hardware-bridge'
version '1.0.1'

sourceCompatibility = '21'
targetCompatibility = '21'

repositories {
    mavenCentral()
}

dependencies {
    // The production code uses the SLF4J logging API at compile time
    implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.23.1'

    implementation group: 'commons-io', name: 'commons-io', version: '2.16.1'
    implementation group: 'commons-codec', name: 'commons-codec', version: '1.17.1'
    implementation group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '5.2.5'
    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.2'
    implementation group: 'com.fazecast', name: 'jSerialComm', version: '2.11.0'
    implementation group: 'org.apache.pdfbox', name: 'pdfbox', version: '2.0.31'
    implementation group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: '1.78.1'
    implementation group: 'org.bouncycastle', name: 'bcpkix-jdk18on', version: '1.78.1'
    implementation group: 'io.javalin', name: 'javalin', version: '6.2.0'
    implementation group: 'io.javalin.community.ssl', name: 'ssl-plugin', version: '6.2.0'

    compileOnly 'org.projectlombok:lombok:1.18.34'
    annotationProcessor 'org.projectlombok:lombok:1.18.34'

    testCompileOnly 'org.projectlombok:lombok:1.18.34'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.34'

    testImplementation group: 'junit', name: 'junit', version: '4.13.2'
}

application {
    mainClass = 'tigerworkshop.webapphardwarebridge.GUI'
}

================================================
FILE: demo/printer-advanced.htm
================================================
<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    <title>WebSocket Printer Advanced</title>
</head>
<body>
<main class="container" role="main">
    <h1 class="mt-5">WebSocket Printer Advanced</h1>

    <hr/>

    <label>PDF by URL</label>
    <div class="input-group mb-3">
        <input class="form-control" id="url" type="text" value="https://pdfobject.com/pdf/sample.pdf"/>
    </div>

    <label>Connection Status</label>

    <input class="form-control mb-3" id="status"/>

    <label>Output</label>

    <input class="form-control mb-3" id="output"/>

    <hr/>

    <div class="py-2">
        <p>
            <button class="btn btn-success" onclick="printPDF()">Normal Print</button>
        </p>

        <hr/>

        <div>
            <button class="btn btn-primary" onclick="printPDFWithFallback()">Print (With Fallback)</button>
            <p class="text-muted">Fallback to window.open() if WebApp Hardware Bridge is not running.</p>
        </div>

        <hr/>

        <div>
            <button class="btn btn-secondary" onclick="printPDFWithId()">Print (With id)</button>
            <p class="text-muted">If &quot;id&quot; is submitted, it will be echoed back in onUpdate() thus we can trace back to jobs we submitted.</p>
        </div>

        <hr/>

        <div class="input-group mb-3">
            <div class="input-group-prepend">
                <input class="form-control" id="qty" type="number" value="3"/>
            </div>

            <button class="btn btn-info" onclick="printPDFWithQty()">Print (With Qty)</button>
        </div>

        <p class="text-muted">If &quot;qty&quot; is submitted, specified copies of documents will be printed.</p>
    </div>
</main>

<script src="websocket-printer.js"></script>
<script>
    var printService = new WebSocketPrinter({
        onConnect: function () {
            document.getElementById('status').value = 'Connected';
        },
        onDisconnect: function () {
            document.getElementById('status').value = 'Disconnected';
        },
        onUpdate: function (message) {
            document.getElementById('output').value = message;
        },
    });

    function printPDF() {
        printService.submit({
            'type': 'INVOICE',
            'url': document.getElementById('url').value
        });
    }

    function printPDFWithId() {
        printService.submit({
            'type': 'INVOICE',
            'id': new Date().getTime(),
            'url': document.getElementById('url').value
        });
    }

    function printPDFWithQty() {
        printService.submit({
            'type': 'INVOICE',
            'qty': document.getElementById('qty').value,
            'url': document.getElementById('url').value
        });
    }


    function printPDFWithFallback() {
        if (printService.isConnected()) {
            printPDF();
        } else {
            alert('WebApp Hardware Bridge not running');
            window.open(document.getElementById('url').value);
        }
    }
</script>
</body>
</html>

================================================
FILE: demo/printer-annotation.htm
================================================
<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    <title>WebSocket Printer Annotation</title>
</head>
<body>
<main class="container" role="main">
    <h1 class="mt-5">WebSocket Printer Annotation</h1>

    <hr/>

    <label>PDF by URL</label>
    <div class="input-group mb-3">
        <input class="form-control" id="url" type="text" value="https://pdfobject.com/pdf/sample.pdf"/>
    </div>

    <label>Connection Status</label>

    <input class="form-control mb-3" id="status"/>

    <label>Output</label>

    <input class="form-control mb-3" id="output"/>

    <hr/>

    <div class="py-2">
<textarea class="form-control" id="extra" rows="4">
[
    {"text": "Hello World!", "x": 10, "y": 10},
    {"text": "This is annotated by WebApp Hardware Bridge", "x": 10, "y": 20, "size": 16, "bold": true}
]
</textarea>

        <p class="text-muted">
            The &quot;extras&quot; attribute is an array of object, allows adding annotation text on top of the PDF/Image before printing.<br/>
            Useful for adding extra text such as timestamp on pre-generated logistic labels.
        </p>

        <p>Example:</p>
<pre>
{
    "text": "Hello World!", // Mandatory
    "x": 10,                // Mandatory
    "y": 10,                // Mandatory
    "size": 12,             // Optional, default 10
    "bold": true,           // Optional, default false
}
</pre>

        <p>
            <button class="btn btn-success" onclick="printPDF()">Print</button>
        </p>
    </div>
</main>

<script src="websocket-printer.js"></script>
<script>
    var printService = new WebSocketPrinter({
        onConnect: function () {
            document.getElementById('status').value = 'Connected';
        },
        onDisconnect: function () {
            document.getElementById('status').value = 'Disconnected';
        },
        onUpdate: function (message) {
            document.getElementById('output').value = message;
        },
    });

    function printPDF() {
        printService.submit({
            'type': 'INVOICE',
            'url': document.getElementById('url').value,
            'extras': JSON.parse(document.getElementById('extra').value),
        });
    }
</script>
</body>
</html>

================================================
FILE: demo/printer-basic.htm
================================================
<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    <title>WebSocket Printer Basic</title>
</head>
<body>
<main class="container" role="main">
    <h1 class="mt-5">WebSocket Printer Basic</h1>

    <hr/>

    <label>PDF by URL</label>
    <div class="input-group mb-3">
        <input class="form-control" id="url" type="text" value="https://pdfobject.com/pdf/sample.pdf"/>

        <div class="input-group-append">
            <button class="btn btn-success" onclick="printPDF()">Print</button>
        </div>
    </div>

    <hr/>

    <label>Image by URL</label>
    <div class="input-group mb-3">
        <input class="form-control" id="url2" type="text" value="https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png"/>

        <div class="input-group-append">
            <button class="btn btn-success" onclick="printImage()">Print</button>
        </div>
    </div>

    <hr/>

    <label>PDF by Base64</label>
    <div class="input-group mb-3">
        <input class="form-control" id="file_content" type="text"
               value="JVBERi0xLjcKJeLjz9MKNiAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDEgMCBSIC9MYXN0TW9kaWZpZWQgKEQ6MjAxNTA4MDIxMjIyMTMrMDAnMDAnKSAvUmVzb3VyY2VzIDIgMCBSIC9NZWRpYUJveCBbMC4wMDAwMDAgMC4wMDAwMDAgNTk1LjI3NjAwMCA4NDEuODkwMDAwXSAvQ3JvcEJveCBbMC4wMDAwMDAgMC4wMDAwMDAgNTk1LjI3NjAwMCA4NDEuODkwMDAwXSAvQmxlZWRCb3ggWzAuMDAwMDAwIDAuMDAwMDAwIDU5NS4yNzYwMDAgODQxLjg5MDAwMF0gL1RyaW1Cb3ggWzAuMDAwMDAwIDAuMDAwMDAwIDU5NS4yNzYwMDAgODQxLjg5MDAwMF0gL0FydEJveCBbMC4wMDAwMDAgMC4wMDAwMDAgNTk1LjI3NjAwMCA4NDEuODkwMDAwXSAvQ29udGVudHMgNyAwIFIgL1JvdGF0ZSAwIC9Hcm91cCA8PCAvVHlwZSAvR3JvdXAgL1MgL1RyYW5zcGFyZW5jeSAvQ1MgL0RldmljZVJHQiA+PiAvQW5ub3RzIFsgNSAwIFIgXSAvUFogMSA+PgplbmRvYmoKNyAwIG9iago8PC9GaWx0ZXIgL0ZsYXRlRGVjb2RlIC9MZW5ndGggMjgwPj4gc3RyZWFtCnictZIxb4MwEIV3fsWNsLi2A4GsbUKrTFTyFjI4tSFUBKhx5Pbf19gqUyolqXrSyacnP/nTPWOUpNgWGMCwtf0Ou709hO1n23XwyOAhp0Axwq6AVbBhAf5Xo72sYDYa6wOrUpwhkmWTlMYJymicLDNgAnYheyrWOWw++WloJWBMoz2w7XXv/QVpRRCNHdFy5YlS7InWsuLnVsPAawlHyYVUwDsBVd/raVQSRDPyQysFnMemq28g/hWHYIIS4nlS7HmI5xmlLlTT6ReHUoZl5HB+5NxhOfkk9bEXI5qBPnyWBMgdUS4uR4myRTIJdn/UDQ6y6I1UdiGHL/CJlqExBum3QVSoV3UZzVCv93/B4MrIL3m/Ae/awBkKZW5kc3RyZWFtCmVuZG9iagoxIDAgb2JqCjw8IC9UeXBlIC9QYWdlcyAvS2lkcyBbIDYgMCBSIF0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwvVHlwZSAvRm9udCAvU3VidHlwZSAvVHlwZTEgL0Jhc2VGb250IC9IZWx2ZXRpY2EgL05hbWUgL0YxIC9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nID4+CmVuZG9iago0IDAgb2JqCjw8L1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1R5cGUxIC9CYXNlRm9udCAvVGltZXMtQm9sZEl0YWxpYyAvTmFtZSAvRjIgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldIC9Gb250IDw8IC9GMSAzIDAgUiAvRjIgNCAwIFIgPj4gL1hPYmplY3QgPDwgPj4gPj4KZW5kb2JqCjUgMCBvYmoKPDwvVHlwZSAvQW5ub3QgL1N1YnR5cGUgL0xpbmsgL1JlY3QgWzIuODM1MDAwIDEuMDAwMDAwIDE5LjAwNTAwMCAyLjE1NjAwMF0gL0NvbnRlbnRzICj+/wBoAHQAdABwADoALwAvAHcAdwB3AC4AdABjAHAAZABmAC4AbwByAGcpIC9QIDYgMCBSIC9OTSAoMDAwMS0wMDAwKSAvTSAoRDoyMDE1MDgwMjEyMjIxMyswMCcwMCcpIC9GIDQgL0JvcmRlciBbMCAwIDBdIC9BIDw8L1MgL1VSSSAvVVJJIChodHRwOi8vd3d3LnRjcGRmLm9yZyk+PiAvSCAvST4+CmVuZG9iago4IDAgb2JqCjw8IC9UaXRsZSAo/v8AVABDAFAARABGACAARQB4AGEAbQBwAGwAZQAgADAAMAAyKSAvQXV0aG9yICj+/wBOAGkAYwBvAGwAYQAgAEEAcwB1AG4AaSkgL1N1YmplY3QgKP7/AFQAQwBQAEQARgAgAFQAdQB0AG8AcgBpAGEAbCkgL0tleXdvcmRzICj+/wBUAEMAUABEAEYALAAgAFAARABGACwAIABlAHgAYQBtAHAAbABlACwAIAB0AGUAcwB0ACwAIABnAHUAaQBkAGUpIC9DcmVhdG9yICj+/wBUAEMAUABEAEYpIC9Qcm9kdWNlciAo/v8AVABDAFAARABGACAANgAuADIALgAxADEAIABcKABoAHQAdABwADoALwAvAHcAdwB3AC4AdABjAHAAZABmAC4AbwByAGcAXCkpIC9DcmVhdGlvbkRhdGUgKEQ6MjAxNTA4MDIxMjIyMTMrMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDE1MDgwMjEyMjIxMyswMCcwMCcpIC9UcmFwcGVkIC9GYWxzZSA+PgplbmRvYmoKOSAwIG9iago8PCAvVHlwZSAvTWV0YWRhdGEgL1N1YnR5cGUgL1hNTCAvTGVuZ3RoIDQzMzYgPj4gc3RyZWFtCjw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMi4xLWMwNDMgNTIuMzcyNzI4LCAyMDA5LzAxLzE4LTE1OjA4OjA0Ij4KCTxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CgkJPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KCQkJPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KCQkJPGRjOnRpdGxlPgoJCQkJPHJkZjpBbHQ+CgkJCQkJPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5UQ1BERiBFeGFtcGxlIDAwMjwvcmRmOmxpPgoJCQkJPC9yZGY6QWx0PgoJCQk8L2RjOnRpdGxlPgoJCQk8ZGM6Y3JlYXRvcj4KCQkJCTxyZGY6U2VxPgoJCQkJCTxyZGY6bGk+Tmljb2xhIEFzdW5pPC9yZGY6bGk+CgkJCQk8L3JkZjpTZXE+CgkJCTwvZGM6Y3JlYXRvcj4KCQkJPGRjOmRlc2NyaXB0aW9uPgoJCQkJPHJkZjpBbHQ+CgkJCQkJPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5UQ1BERiBUdXRvcmlhbDwvcmRmOmxpPgoJCQkJPC9yZGY6QWx0PgoJCQk8L2RjOmRlc2NyaXB0aW9uPgoJCQk8ZGM6c3ViamVjdD4KCQkJCTxyZGY6QmFnPgoJCQkJCTxyZGY6bGk+VENQREYsIFBERiwgZXhhbXBsZSwgdGVzdCwgZ3VpZGU8L3JkZjpsaT4KCQkJCTwvcmRmOkJhZz4KCQkJPC9kYzpzdWJqZWN0PgoJCTwvcmRmOkRlc2NyaXB0aW9uPgoJCTxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CgkJCTx4bXA6Q3JlYXRlRGF0ZT4yMDE1LTA4LTAyVDEyOjIyOjEzKzAwOjAwPC94bXA6Q3JlYXRlRGF0ZT4KCQkJPHhtcDpDcmVhdG9yVG9vbD5UQ1BERjwveG1wOkNyZWF0b3JUb29sPgoJCQk8eG1wOk1vZGlmeURhdGU+MjAxNS0wOC0wMlQxMjoyMjoxMyswMDowMDwveG1wOk1vZGlmeURhdGU+CgkJCTx4bXA6TWV0YWRhdGFEYXRlPjIwMTUtMDgtMDJUMTI6MjI6MTMrMDA6MDA8L3htcDpNZXRhZGF0YURhdGU+CgkJPC9yZGY6RGVzY3JpcHRpb24+CgkJPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIj4KCQkJPHBkZjpLZXl3b3Jkcz5UQ1BERiwgUERGLCBleGFtcGxlLCB0ZXN0LCBndWlkZTwvcGRmOktleXdvcmRzPgoJCQk8cGRmOlByb2R1Y2VyPlRDUERGIDYuMi4xMSAoaHR0cDovL3d3dy50Y3BkZi5vcmcpPC9wZGY6UHJvZHVjZXI+CgkJPC9yZGY6RGVzY3JpcHRpb24+CgkJPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iPgoJCQk8eG1wTU06RG9jdW1lbnRJRD51dWlkOmY2YjgzZWY0LTEyZjItNGE4NS05Yzc3LThlNmY0MTQwOWJmMjwveG1wTU06RG9jdW1lbnRJRD4KCQkJPHhtcE1NOkluc3RhbmNlSUQ+dXVpZDpmNmI4M2VmNC0xMmYyLTRhODUtOWM3Ny04ZTZmNDE0MDliZjI8L3htcE1NOkluc3RhbmNlSUQ+CgkJPC9yZGY6RGVzY3JpcHRpb24+CgkJPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6cGRmYUV4dGVuc2lvbj0iaHR0cDovL3d3dy5haWltLm9yZy9wZGZhL25zL2V4dGVuc2lvbi8iIHhtbG5zOnBkZmFTY2hlbWE9Imh0dHA6Ly93d3cuYWlpbS5vcmcvcGRmYS9ucy9zY2hlbWEjIiB4bWxuczpwZGZhUHJvcGVydHk9Imh0dHA6Ly93d3cuYWlpbS5vcmcvcGRmYS9ucy9wcm9wZXJ0eSMiPgoJCQk8cGRmYUV4dGVuc2lvbjpzY2hlbWFzPgoJCQkJPHJkZjpCYWc+CgkJCQkJPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CgkJCQkJCTxwZGZhU2NoZW1hOm5hbWVzcGFjZVVSST5odHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvPC9wZGZhU2NoZW1hOm5hbWVzcGFjZVVSST4KCQkJCQkJPHBkZmFTY2hlbWE6cHJlZml4PnBkZjwvcGRmYVNjaGVtYTpwcmVmaXg+CgkJCQkJCTxwZGZhU2NoZW1hOnNjaGVtYT5BZG9iZSBQREYgU2NoZW1hPC9wZGZhU2NoZW1hOnNjaGVtYT4KCQkJCQk8L3JkZjpsaT4KCQkJCQk8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KCQkJCQkJPHBkZmFTY2hlbWE6bmFtZXNwYWNlVVJJPmh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS88L3BkZmFTY2hlbWE6bmFtZXNwYWNlVVJJPgoJCQkJCQk8cGRmYVNjaGVtYTpwcmVmaXg+eG1wTU08L3BkZmFTY2hlbWE6cHJlZml4PgoJCQkJCQk8cGRmYVNjaGVtYTpzY2hlbWE+WE1QIE1lZGlhIE1hbmFnZW1lbnQgU2NoZW1hPC9wZGZhU2NoZW1hOnNjaGVtYT4KCQkJCQkJPHBkZmFTY2hlbWE6cHJvcGVydHk+CgkJCQkJCQk8cmRmOlNlcT4KCQkJCQkJCQk8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KCQkJCQkJCQkJPHBkZmFQcm9wZXJ0eTpjYXRlZ29yeT5pbnRlcm5hbDwvcGRmYVByb3BlcnR5OmNhdGVnb3J5PgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5OmRlc2NyaXB0aW9uPlVVSUQgYmFzZWQgaWRlbnRpZmllciBmb3Igc3BlY2lmaWMgaW5jYXJuYXRpb24gb2YgYSBkb2N1bWVudDwvcGRmYVByb3BlcnR5OmRlc2NyaXB0aW9uPgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5Om5hbWU+SW5zdGFuY2VJRDwvcGRmYVByb3BlcnR5Om5hbWU+CgkJCQkJCQkJCTxwZGZhUHJvcGVydHk6dmFsdWVUeXBlPlVSSTwvcGRmYVByb3BlcnR5OnZhbHVlVHlwZT4KCQkJCQkJCQk8L3JkZjpsaT4KCQkJCQkJCTwvcmRmOlNlcT4KCQkJCQkJPC9wZGZhU2NoZW1hOnByb3BlcnR5PgoJCQkJCTwvcmRmOmxpPgoJCQkJCTxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgoJCQkJCQk8cGRmYVNjaGVtYTpuYW1lc3BhY2VVUkk+aHR0cDovL3d3dy5haWltLm9yZy9wZGZhL25zL2lkLzwvcGRmYVNjaGVtYTpuYW1lc3BhY2VVUkk+CgkJCQkJCTxwZGZhU2NoZW1hOnByZWZpeD5wZGZhaWQ8L3BkZmFTY2hlbWE6cHJlZml4PgoJCQkJCQk8cGRmYVNjaGVtYTpzY2hlbWE+UERGL0EgSUQgU2NoZW1hPC9wZGZhU2NoZW1hOnNjaGVtYT4KCQkJCQkJPHBkZmFTY2hlbWE6cHJvcGVydHk+CgkJCQkJCQk8cmRmOlNlcT4KCQkJCQkJCQk8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KCQkJCQkJCQkJPHBkZmFQcm9wZXJ0eTpjYXRlZ29yeT5pbnRlcm5hbDwvcGRmYVByb3BlcnR5OmNhdGVnb3J5PgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5OmRlc2NyaXB0aW9uPlBhcnQgb2YgUERGL0Egc3RhbmRhcmQ8L3BkZmFQcm9wZXJ0eTpkZXNjcmlwdGlvbj4KCQkJCQkJCQkJPHBkZmFQcm9wZXJ0eTpuYW1lPnBhcnQ8L3BkZmFQcm9wZXJ0eTpuYW1lPgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5OnZhbHVlVHlwZT5JbnRlZ2VyPC9wZGZhUHJvcGVydHk6dmFsdWVUeXBlPgoJCQkJCQkJCTwvcmRmOmxpPgoJCQkJCQkJCTxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5OmNhdGVnb3J5PmludGVybmFsPC9wZGZhUHJvcGVydHk6Y2F0ZWdvcnk+CgkJCQkJCQkJCTxwZGZhUHJvcGVydHk6ZGVzY3JpcHRpb24+QW1lbmRtZW50IG9mIFBERi9BIHN0YW5kYXJkPC9wZGZhUHJvcGVydHk6ZGVzY3JpcHRpb24+CgkJCQkJCQkJCTxwZGZhUHJvcGVydHk6bmFtZT5hbWQ8L3BkZmFQcm9wZXJ0eTpuYW1lPgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5OnZhbHVlVHlwZT5UZXh0PC9wZGZhUHJvcGVydHk6dmFsdWVUeXBlPgoJCQkJCQkJCTwvcmRmOmxpPgoJCQkJCQkJCTxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgoJCQkJCQkJCQk8cGRmYVByb3BlcnR5OmNhdGVnb3J5PmludGVybmFsPC9wZGZhUHJvcGVydHk6Y2F0ZWdvcnk+CgkJCQkJCQkJCTxwZGZhUHJvcGVydHk6ZGVzY3JpcHRpb24+Q29uZm9ybWFuY2UgbGV2ZWwgb2YgUERGL0Egc3RhbmRhcmQ8L3BkZmFQcm9wZXJ0eTpkZXNjcmlwdGlvbj4KCQkJCQkJCQkJPHBkZmFQcm9wZXJ0eTpuYW1lPmNvbmZvcm1hbmNlPC9wZGZhUHJvcGVydHk6bmFtZT4KCQkJCQkJCQkJPHBkZmFQcm9wZXJ0eTp2YWx1ZVR5cGU+VGV4dDwvcGRmYVByb3BlcnR5OnZhbHVlVHlwZT4KCQkJCQkJCQk8L3JkZjpsaT4KCQkJCQkJCTwvcmRmOlNlcT4KCQkJCQkJPC9wZGZhU2NoZW1hOnByb3BlcnR5PgoJCQkJCTwvcmRmOmxpPgoJCQkJPC9yZGY6QmFnPgoJCQk8L3BkZmFFeHRlbnNpb246c2NoZW1hcz4KCQk8L3JkZjpEZXNjcmlwdGlvbj4KCTwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cjw/eHBhY2tldCBlbmQ9InciPz4KZW5kc3RyZWFtCmVuZG9iagoxMCAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvVmVyc2lvbiAvMS43IC9QYWdlcyAxIDAgUiAvTmFtZXMgPDwgPj4gL1ZpZXdlclByZWZlcmVuY2VzIDw8IC9EaXJlY3Rpb24gL0wyUiA+PiAvUGFnZUxheW91dCAvU2luZ2xlUGFnZSAvUGFnZU1vZGUgL1VzZU5vbmUgL09wZW5BY3Rpb24gWzYgMCBSIC9GaXRIIG51bGxdIC9NZXRhZGF0YSA5IDAgUiAvTGFuZyAo/v8AZQBuKSA+PgplbmRvYmoKeHJlZgowIDExCjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDgzMyAwMDAwMCBuIAowMDAwMDAxMTExIDAwMDAwIG4gCjAwMDAwMDA4OTIgMDAwMDAgbiAKMDAwMDAwMDk5OCAwMDAwMCBuIAowMDAwMDAxMjI1IDAwMDAwIG4gCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDQ4MyAwMDAwMCBuIAowMDAwMDAxNDk2IDAwMDAwIG4gCjAwMDAwMDE5MjMgMDAwMDAgbiAKMDAwMDAwNjM0MSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDExIC9Sb290IDEwIDAgUiAvSW5mbyA4IDAgUiAvSUQgWyA8ZjZiODNlZjQxMmYyNGE4NTljNzc4ZTZmNDE0MDliZjI+IDxmNmI4M2VmNDEyZjI0YTg1OWM3NzhlNmY0MTQwOWJmMj4gXSA+PgpzdGFydHhyZWYKNjU2NAolJUVPRgo="/>
        <div class="input-group-append">
            <button class="btn btn-success" onclick="printPDFBase64()">Print</button>
        </div>
    </div>

    <hr/>

    <label>Raw (ESC/POS)</label>
    <div class="input-group mb-3">
        <input class="form-control" id="raw_content" type="text"
               value="G0AbQBthAEhlbGxvIFdvcmxkCh0hERthAUVTQy9QT1MgUHJpbnRlciBUZXN0Ch0hABthAkdvb2RieWUgV29ybGQKHVZBAw=="/>
        <div class="input-group-append">
            <button class="btn btn-success" onclick="printRaw()">Print</button>
        </div>
    </div>
</main>

<script src="websocket-printer.js"></script>
<script>
    const printService = new WebSocketPrinter();

    function printPDF() {
        printService.submit({
            'type': 'INVOICE',
            'url': document.getElementById("url").value
        });
    }

    function printImage() {
        printService.submit({
            'type': 'INVOICE',
            'url': document.getElementById("url2").value
        });
    }

    function printPDFBase64() {
        printService.submit({
            'type': 'INVOICE',
            'url': 'file.pdf',
            'file_content': document.getElementById('file_content').value
        });
    }

    function printRaw() {
        printService.submit({
            'type': 'RECEIPT',
            'raw_content': document.getElementById('raw_content').value
        });
    }
</script>
</body>
</html>

================================================
FILE: demo/serial-basic.html
================================================
<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    <title>WebSocket Serial</title>
</head>
<body>
<main class="container" role="main">
    <h1 class="mt-5">WebSocket Serial</h1>

    <form id="form">
        <p>Input:</p>
        <div class="input-group mb-3">
            <input class="form-control" id="input" name="input" placeholder="Input" type="text" value=""/>

            <div class="input-group-append">
                <button class="btn btn-success" type="submit">Send</button>
            </div>
        </div>

        <p>Output:</p>
        <textarea class="form-control" rows="10" id="output" name="output"></textarea>
    </form>

    <script src="websocket-serial.js"></script>
    <script>
        const $form = document.getElementById("form");
        const $input = document.getElementById("input");
        const $output = document.getElementById("output");

        const serial = new WebSocketSerial({
            url: 'ws://127.0.0.1:12212/serial/DISPLAY',
            onMessage: function (message) {
                $output.value = $output.value + message;
            }
        });

        $form.onsubmit = function (e) {
            serial.send($input.value);
            $input.value = '';
            e.preventDefault();
        }
    </script>
</main>
</body>
</html>

================================================
FILE: demo/serial-weigh.htm
================================================
<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    <title>WebSocket Serial Weigh (AWH-30)</title>
</head>
<body>
<main class="container" role="main">
    <h1 class="mt-5">WebSocket Serial Weigh (AWH-30)</h1>

    <hr/>

    <input class="form-control" id="weight" type="text"/>
</main>

<script src="websocket-weigh.js"></script>
<script>
    const $element = document.getElementById("weight");
    WebSocketWeigh({
        onUpdate: function (weight, stable) {
            $element.value = weight;
            if (stable) {
                $element.classList.add("bg-success")
                $element.classList.remove("bg-warning");
            } else {
                $element.classList.add("bg-warning")
                $element.classList.remove("bg-success");
            }
        }
    });
</script>
</body>
</html>

================================================
FILE: demo/websocket-printer.js
================================================
function WebSocketPrinter(options) {
    var defaults = {
        url: "ws://127.0.0.1:12212/printer",
        onConnect: function () {
        },
        onDisconnect: function () {
        },
        onUpdate: function () {
        },
    };

    var settings = Object.assign({}, defaults, options);
    var websocket;
    var connected = false;

    var onMessage = function (evt) {
        settings.onUpdate(evt.data);
    };

    var onConnect = function () {
        connected = true;
        settings.onConnect();
    };

    var onDisconnect = function () {
        connected = false;
        settings.onDisconnect();
        reconnect();
    };

    var connect = function () {
        websocket = new WebSocket(settings.url);
        websocket.onopen = onConnect;
        websocket.onclose = onDisconnect;
        websocket.onmessage = onMessage;
    };

    var reconnect = function () {
        connect();
    };

    this.submit = function (data) {
        if (Array.isArray(data)) {
            data.forEach(function (element) {
                websocket.send(JSON.stringify(element));
            });
        } else {
            websocket.send(JSON.stringify(data));
        }
    };

    this.isConnected = function () {
        return connected;
    };

    connect();
}

================================================
FILE: demo/websocket-serial.js
================================================
function WebSocketSerial(options) {
    var defaults = {
        url: 'ws://127.0.0.1:12212/serial/DISPLAY',
        onConnect: function () {
        },
        onDisconnect: function () {
        },
        onMessage: function (message) {
        }
    };

    var settings = Object.assign({}, defaults, options);
    var websocket;
    var buffer = '';

    var onMessage = function (evt) {
        var chr = evt.data;
        settings.onMessage(chr);
    };

    var onConnect = function () {
        settings.onConnect();
    };

    var onDisconnect = function () {
        settings.onDisconnect();
        reconnect();
    };

    var connect = function () {
        websocket = new WebSocket(settings.url);
        websocket.onopen = onConnect;
        websocket.onclose = onDisconnect;
        websocket.onmessage = onMessage;
    };

    var reconnect = function () {
        connect();
    };

    this.send = function (message) {
        websocket.send(message);
    };

    connect();
}

================================================
FILE: demo/websocket-weigh.js
================================================
function WebSocketWeigh(options) {
    var defaults = {
        url: 'ws://127.0.0.1:12212/serial/WEIGH',
        weightRegex: new RegExp('([0-9]{1,2}\\.[0-9]{3})kg'),
        stableRegex: new RegExp('^ST.*\\s+'),
        onConnect: function () {
        },
        onDisconnect: function () {
        },
        onUpdate: function (weight, stable) {

        }
    };

    var settings = Object.assign({}, defaults, options);
    var websocket;
    var buffer = '';

    var onMessage = function (evt) {
        var chr = evt.data;
        if (chr == "\n") {
            var weightOutput = settings.weightRegex.exec(buffer);
            var stableOutput = settings.stableRegex.test(buffer);

            if (weightOutput != null) {
                settings.onUpdate(weightOutput[1], stableOutput);
            }
            buffer = '';
        } else {
            buffer = buffer + chr;
        }
    };

    var onConnect = function () {
        settings.onConnect();
    };

    var onDisconnect = function () {
        settings.onDisconnect();
        reconnect();
    };

    var connect = function () {
        websocket = new WebSocket(settings.url);
        websocket.onopen = onConnect;
        websocket.onclose = onDisconnect;
        websocket.onmessage = onMessage;
    };

    var reconnect = function () {
        connect();
    };

    connect();
}

================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: install.nsi
================================================
; The name of the installer
Name "WebApp Hardware Bridge"

; The file to write
OutFile "whb.exe"

; The default installation directory
InstallDir "$LOCALAPPDATA\WebApp Hardware Bridge"

; Request application privileges for Windows Vista
RequestExecutionLevel user

;--------------------------------

; Pages

;Page directory
Page components
Page instfiles

;--------------------------------

; The stuff to install
Section "!Main Application" ;No components page, name is not important
  SectionIn RO

  ; Set output path to the installation directory.
  SetOutPath $INSTDIR
  
  ; Remove old version
  RMDir /r "$INSTDIR\jre"
  Delete "$INSTDIR\*.jar"
  Delete "$INSTDIR\setting.default.json"
  Delete "$DESKTOP\WebApp Hardware Bridge (GUI).lnk"
  Delete "$DESKTOP\WebApp Hardware Bridge (Configurator).lnk"
  Delete "$SMPROGRAMS\WebApp Hardware Bridge (GUI).lnk"
  Delete "$SMPROGRAMS\WebApp Hardware Bridge (Configurator).lnk"
  
  ; Put file there
  File /r out\artifacts\webapp_hardware_bridge_jar\*
  File /r jre
  
  File "install.nsi"
  File "icon.ico"
  
  ; Delete shortcuts  
  Delete "$DESKTOP\WebApp Hardware Bridge.lnk"
  Delete "$DESKTOP\WebApp Hardware Bridge (CLI).lnk"
  Delete "$SMPROGRAMS\WebApp Hardware Bridge.lnk"
  Delete "$SMPROGRAMS\WebApp Hardware Bridge (CLI).lnk"
  
  ; Create shortcuts
  CreateShortcut "$DESKTOP\WebApp Hardware Bridge.lnk" "$INSTDIR\jre\bin\javaw.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI" "$INSTDIR\icon.ico" 0
  CreateShortcut "$DESKTOP\WebApp Hardware Bridge (CLI).lnk" "$INSTDIR\jre\bin\java.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.Server" "$INSTDIR\icon.ico" 0
  CreateShortcut "$SMPROGRAMS\WebApp Hardware Bridge.lnk" "$INSTDIR\jre\bin\javaw.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI" "$INSTDIR\icon.ico" 0
  CreateShortcut "$SMPROGRAMS\WebApp Hardware Bridge (CLI).lnk" "$INSTDIR\jre\bin\java.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.Server" "$INSTDIR\icon.ico" 0

  ; Write the installation path into the registry
  WriteRegStr HKCU "SOFTWARE\WebApp Hardware Bridge" "Install_Dir" "$INSTDIR"
  
  ; Write the uninstall keys for Windows
  WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "DisplayName" "WebApp Hardware Bridge"
  WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "UninstallString" '"$INSTDIR\uninstall.exe"'
  WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "NoModify" 1
  WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge" "NoRepair" 1
  WriteUninstaller "uninstall.exe"

  ; Auto close when finished
  SetAutoClose true
SectionEnd ; end the section

Section "Auto-start" autostart
  CreateShortcut "$SMSTARTUP\WebApp Hardware Bridge.lnk" "$INSTDIR\jre\bin\javaw.exe" "-cp webapp-hardware-bridge.jar tigerworkshop.webapphardwarebridge.GUI"
SectionEnd

Section "Uninstall"
  ; Remove registry keys
  DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\WebApp Hardware Bridge"
  DeleteRegKey HKCU "SOFTWARE\WebApp Hardware Bridge"
  
  ; Delete shortcuts
  Delete "$DESKTOP\WebApp Hardware Bridge.lnk"
  Delete "$DESKTOP\WebApp Hardware Bridge (CLI).lnk"
  Delete "$SMPROGRAMS\WebApp Hardware Bridge.lnk"
  Delete "$SMPROGRAMS\WebApp Hardware Bridge (CLI).lnk"
  
  ; Remove files and uninstaller
  RMDir /r $INSTDIR
SectionEnd

Function .onInstSuccess
  ExecShell "" "$DESKTOP\WebApp Hardware Bridge.lnk"
FunctionEnd

================================================
FILE: settings.gradle
================================================
rootProject.name = 'webapp-hardware-bridge'

================================================
FILE: src/main/java/module-info.java
================================================
module tigerworkshop.webapphardwarebridge {
    requires java.desktop;
    requires com.fazecast.jSerialComm;
    requires jdk.management;
    requires org.bouncycastle.provider;
    requires org.bouncycastle.pkix;
    requires org.apache.commons.io;
    requires org.slf4j;
    requires org.apache.pdfbox;
    requires org.apache.commons.codec;
    requires org.apache.httpcomponents.core5.httpcore5;
    requires org.apache.logging.log4j;
    requires io.javalin;
    requires com.fasterxml.jackson.databind;
    requires io.javalin.community.ssl;
    requires static lombok;

    opens tigerworkshop.webapphardwarebridge.dtos to com.fasterxml.jackson.databind;
    opens tigerworkshop.webapphardwarebridge.responses to com.fasterxml.jackson.databind;
    opens tigerworkshop.webapphardwarebridge.utils to com.fasterxml.jackson.databind;

    exports tigerworkshop.webapphardwarebridge;
    exports tigerworkshop.webapphardwarebridge.interfaces;
}

================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/Constants.java
================================================
package tigerworkshop.webapphardwarebridge;

public class Constants {
    public static final String APP_NAME = "WebApp Hardware Bridge";
    public static final String APP_ID = "tigerworkshop.webapphardwarebridge";
    public static final String VERSION = "1.0.1";
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/GUI.java
================================================
package tigerworkshop.webapphardwarebridge;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import tigerworkshop.webapphardwarebridge.dtos.Config;
import tigerworkshop.webapphardwarebridge.dtos.NotificationDTO;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
import tigerworkshop.webapphardwarebridge.services.ConfigService;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.net.URI;
import java.util.Objects;

@Log4j2
public class GUI implements WebSocketServiceInterface {
    private static final ConfigService configService = ConfigService.getInstance();

    private final Server server = new Server();
    private Config config = configService.getConfig();

    Desktop desktop = Desktop.getDesktop();
    TrayIcon trayIcon;
    SystemTray tray;

    public static void main(String[] args) throws Exception {
        GUI gui = new GUI();
        gui.launch();
    }

    public void launch() throws Exception {
        server.start();

        // Create tray icon
        if (!SystemTray.isSupported()) {
            log.warn("SystemTray is not supported");
            return;
        }

        // Register service as notification listener
        if (config.getGui().getNotification().isEnabled()) {
            server.registerService(this);
        }

        MenuItem settingItem = new MenuItem("Web UI");
        settingItem.addActionListener(e -> {
            try {
                if (desktop == null || !desktop.isSupported(Desktop.Action.BROWSE)) {
                    throw new Exception("Desktop browse is not supported");
                }

                desktop.browse(new URI(config.getServer().getUri()));
            } catch (Exception ex) {
                log.error("Failed to open Web UI", ex);
            }
        });

        MenuItem appDirectoryItem = new MenuItem("App Directory");
        appDirectoryItem.addActionListener(e -> {
            try {
                if (desktop == null || !desktop.isSupported(Desktop.Action.OPEN)) {
                    throw new Exception("Desktop open is not supported");
                }

                desktop.open(new File("."));
            } catch (Exception ex) {
                log.error("Failed to open log folder", ex);
            }
        });

        MenuItem logDirectoryItem = new MenuItem("Log Directory");
        logDirectoryItem.addActionListener(e -> {
            try {
                if (desktop == null || !desktop.isSupported(Desktop.Action.OPEN)) {
                    throw new Exception("Desktop open is not supported");
                }

                desktop.open(new File("log"));
            } catch (Exception ex) {
                log.error("Failed to open log folder", ex);
            }
        });

        MenuItem restartItem = new MenuItem("Restart");
        restartItem.addActionListener(e -> restart());

        MenuItem exitItem = new MenuItem("Exit");
        exitItem.addActionListener(e -> System.exit(0));

        // Add components to pop-up menu
        final PopupMenu popupMenu = new PopupMenu();
        popupMenu.add(settingItem);
        popupMenu.addSeparator();
        popupMenu.add(appDirectoryItem);
        popupMenu.add(logDirectoryItem);
        popupMenu.addSeparator();
        popupMenu.add(restartItem);
        popupMenu.add(exitItem);

        tray = SystemTray.getSystemTray();

        // Set icon
        Dimension trayIconSize = tray.getTrayIconSize();
        final Image image = ImageIO.read(Objects.requireNonNull(getClass().getClassLoader().getResource("icon.png")));
        final Image scaledImage = image.getScaledInstance(trayIconSize.width, trayIconSize.height, Image.SCALE_SMOOTH);

        trayIcon = new TrayIcon(scaledImage, Constants.APP_NAME);
        trayIcon.setPopupMenu(popupMenu);

        tray.add(trayIcon);

        notify(Constants.APP_NAME, " is running in background!", TrayIcon.MessageType.INFO);
    }

    public void notify(String title, String message, TrayIcon.MessageType messageType) {
        try {
            trayIcon.displayMessage(title, message, messageType);
        } catch (Exception e) {
            log.error("Failed to display notification", e);
        }
    }

    public void restart() {
        try {
            config = configService.getConfig();

            server.stop();
            server.start();

            notify("Restart", "Server restarted successfully", TrayIcon.MessageType.INFO);
        } catch (Exception e) {
            log.error("Failed to restart server", e);
        }
    }

    @Override
    public void start() {

    }

    @Override
    public void stop() {

    }

    @Override
    public void messageToService(String message) {
        try {
            log.debug("GUI Notification: {}", message);

            NotificationDTO notificationDTO = new ObjectMapper().readValue(message, NotificationDTO.class);
            notify(notificationDTO.getTitle(), notificationDTO.getMessage(), TrayIcon.MessageType.valueOf(notificationDTO.getType()));
        } catch (Exception e) {
            log.error("Failed to parse notification message", e);
        }
    }

    @Override
    public void messageToService(byte[] message) {
    }

    @Override
    public void onRegister(WebSocketServerInterface server) {

    }

    @Override
    public void onUnregister() {
    }

    @Override
    public String getChannel() {
        return "/notification";
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/Server.java
================================================
package tigerworkshop.webapphardwarebridge;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fazecast.jSerialComm.SerialPort;
import io.javalin.Javalin;
import io.javalin.community.ssl.SslPlugin;
import io.javalin.http.ContentType;
import io.javalin.plugin.bundled.CorsPluginConfig;
import io.javalin.util.JavalinBindException;
import io.javalin.websocket.WsContext;
import lombok.extern.log4j.Log4j2;
import tigerworkshop.webapphardwarebridge.dtos.*;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
import tigerworkshop.webapphardwarebridge.services.ConfigService;
import tigerworkshop.webapphardwarebridge.utils.CertificateGenerator;
import tigerworkshop.webapphardwarebridge.utils.ThreadUtil;
import tigerworkshop.webapphardwarebridge.websocketservices.PrinterWebSocketService;
import tigerworkshop.webapphardwarebridge.websocketservices.SerialWebSocketService;

import javax.print.PrintService;
import java.awt.print.PrinterJob;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

@Log4j2
public class Server implements WebSocketServerInterface {
    private Javalin javalinServer;

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final ConfigService configService = ConfigService.getInstance();

    private final ConcurrentHashMap<String, ConcurrentLinkedQueue<WsContext>> socketChannelSubscriptions = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, ConcurrentLinkedQueue<WebSocketServiceInterface>> serviceChannelSubscriptions = new ConcurrentHashMap<>();
    private final ConcurrentLinkedQueue<WebSocketServiceInterface> services = new ConcurrentLinkedQueue<>();

    public static void main(String[] args) {
        try {
            new Server().start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    synchronized public void start() throws Exception {
        Config config = configService.getConfig();

        Config.Server serverConfig = config.getServer();

        // Create Javalin Server
        javalinServer = Javalin.create(cfg -> {
            cfg.showJavalinBanner = false;
            cfg.staticFiles.add(staticFiles -> staticFiles.directory = "web");
            cfg.bundledPlugins.enableCors(cors -> cors.addRule(CorsPluginConfig.CorsRule::anyHost));

            if (serverConfig.getTls().isEnabled()) {
                if (serverConfig.getTls().isSelfSigned()) {
                    log.info("TLS Enabled with self-signed certificate");

                    CertificateGenerator.generateSelfSignedCertificate(serverConfig.getAddress(), serverConfig.getTls().getCert(), serverConfig.getTls().getKey());

                    log.info("For first time setup, open in browser and trust the certificate: {}", serverConfig.getUri());
                }

                SslPlugin plugin = new SslPlugin(conf -> {
                    conf.insecure = false;
                    conf.securePort = serverConfig.getPort();
                    conf.pemFromPath(serverConfig.getTls().getCert(), serverConfig.getTls().getKey());
                    conf.sniHostCheck = !serverConfig.getTls().isSelfSigned();
                });
                cfg.registerPlugin(plugin);
            }
        });

        // Add WebSocket Auth
        javalinServer.wsBefore(ctx -> {
            ctx.onConnect(wsConnectContext -> {
                wsConnectContext.session.getPolicy().setMaxBinaryMessageSize(-1);
                wsConnectContext.session.getPolicy().setMaxTextMessageSize(-1);

                wsConnectContext.enableAutomaticPings(5, TimeUnit.SECONDS);

                if (serverConfig.getAuthentication().isEnabled()) {
                    if (Optional.ofNullable(wsConnectContext.queryParam("token")).orElse("").equals(serverConfig.getAuthentication().getToken())) {
                        return;
                    }

                    wsConnectContext.closeSession(1003, "Invalid token");
                }
            });
        });

        // Add WebSocket Printer Service
        Config.Printer printerConfig = config.getPrinter();
        if (printerConfig.isEnabled()) {
            PrinterWebSocketService printerWebSocketService = new PrinterWebSocketService();
            printerWebSocketService.start();

            javalinServer.ws(printerWebSocketService.getChannel(), ws -> {
                ws.onConnect(ctx -> {
                    log.info("{} connected to {}", ctx.host(), printerWebSocketService.getChannel());

                    addSocketToChannel(printerWebSocketService.getChannel(), ctx);
                });

                ws.onClose(ctx -> {
                    log.info("{} disconnected from {}", ctx.host(), printerWebSocketService.getChannel());

                    removeSocketFromChannel(printerWebSocketService.getChannel(), ctx);
                });

                ws.onMessage(ctx -> {
                    log.info("{} sent message to {}: {}", ctx.host(), printerWebSocketService.getChannel(), ctx.message());

                    messageToService("/printer", ctx.message());
                });
            });

            registerService(printerWebSocketService);
        }

        // Add WebSocket Serial Service
        Config.Serial serialConfig = config.getSerial();
        if (serialConfig.isEnabled()) {
            serialConfig.getMappings().forEach(mapping -> {
                try {
                    log.info("Starting SerialWebSocketService: {}", mapping.toString());
                    SerialWebSocketService serialWebSocketService = new SerialWebSocketService(mapping);
                    serialWebSocketService.start();

                    registerService(serialWebSocketService);

                    javalinServer.ws(serialWebSocketService.getChannel(), ws -> {
                        ws.onConnect(ctx -> {
                            log.info("{} connected to {}", ctx.host(), serialWebSocketService.getChannel());

                            addSocketToChannel(serialWebSocketService.getChannel(), ctx);
                        });

                        ws.onClose(ctx -> {
                            log.info("{} disconnected from {}", ctx.host(), serialWebSocketService.getChannel());

                            removeSocketFromChannel(serialWebSocketService.getChannel(), ctx);
                        });

                        ws.onMessage(ctx -> {
                            log.info("{} sent message to {}: {}", ctx.host(), serialWebSocketService.getChannel(), ctx.message());

                            messageToService(serialWebSocketService.getChannel(), ctx.message());
                        });

                        ws.onBinaryMessage(ctx -> {
                            log.info("{} sent binary message to {}: {}", ctx.host(), serialWebSocketService.getChannel(), ctx.data());

                            messageToService(serialWebSocketService.getChannel(), ctx.data());
                        });
                    });
                } catch (Exception e) {
                    String message = "Failed to start SerialWebSocketService for " + mapping.getType() + ": " + e.getMessage();
                    log.error(message);

                    try {
                        messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("ERROR", "Serial", message)));
                    } catch (JsonProcessingException ex) {
                        log.error("Failed to send notification: {}", ex.getMessage());
                    }
                }
            });
        }

        // Add HTTP Auth
        javalinServer.before(ctx -> {
            if (serverConfig.getAuthentication().isEnabled()) {
                try {
                    // Bearer Token
                    if (Optional.ofNullable(ctx.header("Authorization")).orElse("").endsWith(serverConfig.getAuthentication().getToken())) {
                        return;
                    }

                    // Basic Auth
                    if (ctx.basicAuthCredentials() != null && Objects.equals(ctx.basicAuthCredentials().getPassword(), serverConfig.getAuthentication().getToken())) {
                        return;
                    }
                } catch (Exception e) {
                    // NOOP
                }

                ctx.header("WWW-Authenticate", "Basic realm=\"Token required\"");
                ctx.res().sendError(401, "Token mismatch");
            }
        });

        // Add HTTP Service
        javalinServer.get("/config.json", ctx -> {
            ctx.contentType(ContentType.APPLICATION_JSON).result(configService.getConfig().toJson());
        });

        javalinServer.put("/config.json", ctx -> {
            configService.loadFromJson(ctx.body());
            configService.save();

            messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("INFO", "Setting", "Setting saved successfully")));

            ctx.contentType(ContentType.APPLICATION_JSON).result(configService.getConfig().toJson());
        });

        javalinServer.get("/system/printers.json", ctx -> {
            ArrayList<PrintServiceDTO> dtos = new ArrayList<>();
            for (PrintService service : PrinterJob.lookupPrintServices()) {
                dtos.add(new PrintServiceDTO(service.getName(), ""));
            }

            ctx.contentType(ContentType.APPLICATION_JSON).result(objectMapper.writeValueAsString(dtos));
        });

        javalinServer.get("/system/serials.json", ctx -> {
            ArrayList<SerialPortDTO> dtos = new ArrayList<>();
            for (SerialPort port : SerialPort.getCommPorts()) {
                dtos.add(new SerialPortDTO(port.getSystemPortName(), port.getPortDescription(), port.getManufacturer()));
            }

            ctx.contentType(ContentType.APPLICATION_JSON).result(objectMapper.writeValueAsString(dtos));
        });

        javalinServer.get("/system/version.json", ctx -> {
            VersionDTO dto = new VersionDTO(Constants.APP_NAME, Constants.APP_ID, Constants.VERSION);

            ctx.contentType(ContentType.APPLICATION_JSON).result(objectMapper.writeValueAsString(dto));
        });

        javalinServer.post("/system/restart.json", ctx -> {
            stop();
            ThreadUtil.silentSleep(500);
            start();

            messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("INFO", "Restart", "Server restarted successfully")));
        });

        try {
            javalinServer.start(serverConfig.getBind(), serverConfig.getPort());
            log.info("{} {} running on {}", Constants.APP_NAME, Constants.VERSION, serverConfig.getUri());
        } catch (JavalinBindException e) {
            log.info("Unable to bind port, another instance is already running?");
            System.exit(1);
        }
    }

    synchronized public void stop() throws Exception {
        for (Iterator<WebSocketServiceInterface> it = services.iterator(); it.hasNext(); ) {
            WebSocketServiceInterface service = it.next();
            unregisterService(service);
            service.stop();
            it.remove();
        }

        javalinServer.stop();
    }

    /*
     * Service to Server listener
     */
    @Override
    public void messageToServer(String channel, String message) {
        log.debug("Received data from channel: {}, Data: {}", channel, message);

        ConcurrentLinkedQueue<WsContext> connectionList = socketChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());

        for (Iterator<WsContext> it = connectionList.iterator(); it.hasNext(); ) {
            try {
                WsContext conn = it.next();
                conn.send(message);
            } catch (Exception e) {
                log.warn("Exception {}: {}, removing connection from list", e.getClass().getSimpleName(), e.getMessage());
                it.remove();
            }
        }
    }

    @Override
    public void messageToServer(String channel, byte[] message) {
        log.debug("Received data from channel: {}, Data: {}", channel, message);

        ConcurrentLinkedQueue<WsContext> connectionList = socketChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());

        for (Iterator<WsContext> it = connectionList.iterator(); it.hasNext(); ) {
            WsContext conn = it.next();
            try {
                conn.send(ByteBuffer.wrap(message));
            } catch (Exception e) {
                log.warn("Exception: Removing connection from list");
                it.remove();
            }
        }
    }

    /*
     * Service to Service listener
     */
    @Override
    public void messageToService(String channel, String message) {
        ConcurrentLinkedQueue<WebSocketServiceInterface> services = getServicesForChannel(channel);
        for (WebSocketServiceInterface service : services) {
            log.debug("Sending: {} to channel: {}, service = {}", message, channel, service.getClass().getSimpleName());

            service.messageToService(message);
        }
    }

    @Override
    public void messageToService(String channel, byte[] bytes) {
        ConcurrentLinkedQueue<WebSocketServiceInterface> services = getServicesForChannel(channel);
        for (WebSocketServiceInterface service : services) {
            log.debug("Sending: {} to channel: {}, service = {}", bytes, channel, service.getClass().getSimpleName());

            service.messageToService(bytes);
        }
    }

    @Override
    public void registerService(WebSocketServiceInterface service) {
        service.onRegister(this);
        addServiceToChannel(service.getChannel(), service);
    }

    @Override
    public void unregisterService(WebSocketServiceInterface service) {
        service.onUnregister();
        removeServiceFromChannel(service.getChannel(), service);
    }

    /*
     * Socket to Channel operations
     */
    private ConcurrentLinkedQueue<WsContext> getSocketsForChannel(String channel) {
        return socketChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());
    }

    void addSocketToChannel(String channel, WsContext socket) {
        ConcurrentLinkedQueue<WsContext> connectionList = getSocketsForChannel(channel);
        connectionList.add(socket);
        socketChannelSubscriptions.put(channel, connectionList);
    }

    private void removeSocketFromChannel(String channel, WsContext socket) {
        ConcurrentLinkedQueue<WsContext> connectionList = getSocketsForChannel(channel);
        connectionList.remove(socket);
        socketChannelSubscriptions.put(channel, connectionList);
    }

    /*
     * Service to Channel operations
     */
    private ConcurrentLinkedQueue<WebSocketServiceInterface> getServicesForChannel(String channel) {
        ConcurrentLinkedQueue<WebSocketServiceInterface> services = new ConcurrentLinkedQueue<>();

        services.addAll(serviceChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>()));
        services.addAll(serviceChannelSubscriptions.getOrDefault("*", new ConcurrentLinkedQueue<>()));

        return services;
    }

    private void addServiceToChannel(String channel, WebSocketServiceInterface service) {
        ConcurrentLinkedQueue<WebSocketServiceInterface> serviceList = serviceChannelSubscriptions.getOrDefault(channel, new ConcurrentLinkedQueue<>());

        serviceList.add(service);
        serviceChannelSubscriptions.put(channel, serviceList);

        if (!services.contains(service)) {
            services.add(service);
        }
    }

    private void removeServiceFromChannel(String channel, WebSocketServiceInterface service) {
        ConcurrentLinkedQueue<WebSocketServiceInterface> serviceList = getServicesForChannel(channel);
        serviceList.remove(service);
        serviceChannelSubscriptions.put(channel, serviceList);

        services.remove(service);
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/Config.java
================================================
package tigerworkshop.webapphardwarebridge.dtos;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

@Data
@NoArgsConstructor
public class Config {
    private GUI gui = new GUI();
    private Server server = new Server();
    private Downloader downloader = new Downloader();
    private Printer printer = new Printer();
    private Serial serial = new Serial();

    public String toJson() throws JsonProcessingException {
        return new ObjectMapper().writeValueAsString(this);
    }

    @Data
    @NoArgsConstructor
    public static class GUI {
        private Notification notification = new Notification();
    }

    @Data
    @NoArgsConstructor
    public static class Notification {
        private boolean enabled = true;
    }

    @Data
    @NoArgsConstructor
    public static class Server {
        private String address = "127.0.0.1";
        private String bind = "127.0.0.1";
        private int port = 12212;
        private Authentication authentication = new Authentication();
        private TLS tls = new TLS();

        @JsonIgnore
        public String getUri() {
            return (tls.isEnabled() ? "https://" : "http://") + address + ":" + port;
        }
    }

    @Data
    @NoArgsConstructor
    public static class Authentication {
        private boolean enabled = false;
        private String token = null;
    }

    @Data
    @NoArgsConstructor
    public static class TLS {
        private boolean enabled = false ;
        private boolean selfSigned = true;
        private String cert = "tls/default-cert.pem";
        private String key = "tls/default-key.pem";
        private String caBundle = null;
    }

    @Data
    @NoArgsConstructor
    public static class Downloader {
        private boolean ignoreTLSCertificateError = false;
        private double timeout = 30;
        private String path = "downloads";
    }

    @Data
    @NoArgsConstructor
    public static class Printer {
        private boolean enabled = true;
        private boolean autoAddUnknownType = false;
        private boolean fallbackToDefault = false;
        private ArrayList<PrinterMapping> mappings = new ArrayList<>();
    }

    @Data
    @NoArgsConstructor
    public static class Serial {
        private boolean enabled = true;
        private ArrayList<SerialMapping> mappings = new ArrayList<>();
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class PrinterMapping {
        private String type;
        private String name;

        private boolean autoRotate = false;
        private boolean resetImageableArea = true;
        private int forceDPI = 0;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class SerialMapping {
        private String type;
        private String name;

        private Integer baudRate;
        private Integer numDataBits;
        private Integer numStopBits;
        private Integer parity;

        private Boolean readMultipleBytes = false;
        private String readCharset = StandardCharsets.UTF_8.toString();
    }
}

================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/NotificationDTO.java
================================================
package tigerworkshop.webapphardwarebridge.dtos;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class NotificationDTO {
    public String type;
    public String title;
    public String message;
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/PrintServiceDTO.java
================================================
package tigerworkshop.webapphardwarebridge.dtos;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
public class PrintServiceDTO {
    public String name;
    public String description;
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/SerialPortDTO.java
================================================
package tigerworkshop.webapphardwarebridge.dtos;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
public class SerialPortDTO {
    public String name;
    public String description;
    public String manufacturer;
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/VersionDTO.java
================================================
package tigerworkshop.webapphardwarebridge.dtos;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
public class VersionDTO {
    public String appName;
    public String appId;
    public String version;
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServerInterface.java
================================================
package tigerworkshop.webapphardwarebridge.interfaces;


public interface WebSocketServerInterface {
    void messageToServer(String channel, String message);

    void messageToServer(String channel, byte[] message);

    void messageToService(String channel, String message);

    void messageToService(String channel, byte[] message);

    void registerService(WebSocketServiceInterface service);

    void unregisterService(WebSocketServiceInterface service);
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServiceInterface.java
================================================
package tigerworkshop.webapphardwarebridge.interfaces;

public interface WebSocketServiceInterface {
    void start();

    void stop();

    void messageToService(String message);

    void messageToService(byte[] message);

    void onRegister(WebSocketServerInterface server);

    void onUnregister();

    String getChannel();
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintDocument.java
================================================
package tigerworkshop.webapphardwarebridge.responses;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.ToString;
import tigerworkshop.webapphardwarebridge.utils.AnnotatedPrintable;

import java.util.ArrayList;
import java.util.UUID;

@ToString
@Getter
public class PrintDocument {
    String type;
    String url;
    String id;
    UUID uuid = UUID.randomUUID();
    Integer qty = 1;
    @JsonProperty("file_content") String fileContent;
    @JsonProperty("raw_content") String rawContent;
    ArrayList<AnnotatedPrintable.AnnotatedPrintableAnnotation> extras = new ArrayList<>();
}

================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintResult.java
================================================
package tigerworkshop.webapphardwarebridge.responses;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;

@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PrintResult {
    public Boolean success;
    public String message;
    public String id;
    public String printerName;
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/services/ConfigService.java
================================================
package tigerworkshop.webapphardwarebridge.services;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import tigerworkshop.webapphardwarebridge.dtos.Config;

import java.io.File;
import java.io.IOException;

@Log4j2
public class ConfigService {
    @Getter
    private static final ConfigService instance = new ConfigService();

    private static final String CONFIG_FILENAME = "config.json";
    private static final String PRINTER_PLACEHOLDER = "";

    private final ObjectMapper objectMapper = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    @Getter
    private Config config = new Config();

    private ConfigService() {
        try {
            loadFromFile(CONFIG_FILENAME);
        } catch (Exception e) {
            log.warn("Failed loading config, creating new file");
            save();
        }
    }

    public void loadFromJson(String json) throws JsonProcessingException {
        log.info("Loading config from JSON: {}", json);
        config = objectMapper.readValue(json, Config.class);
    }

    public void loadFromFile(String filename) throws IOException {
        log.info("Loading config from file: {}", filename);
        config = objectMapper.readValue(new File(filename), Config.class);
    }

    public void save() {
        try {
            objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(CONFIG_FILENAME), config);
        } catch (Exception e) {
            log.error("Failed to save config file", e);
        }
    }

    public void addPrintTypeToList(String printType) {
        config.getPrinter().getMappings().add(new Config.PrinterMapping(printType, PRINTER_PLACEHOLDER, false, true, 0));
        save();
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/services/DocumentService.java
================================================
package tigerworkshop.webapphardwarebridge.services;

import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import tigerworkshop.webapphardwarebridge.dtos.Config;
import tigerworkshop.webapphardwarebridge.responses.PrintDocument;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.security.cert.X509Certificate;
import java.util.Base64;

@Log4j2
public class DocumentService {
    @Getter
    private static final DocumentService instance = new DocumentService();
    private static final Config.Downloader downloaderConfig = ConfigService.getInstance().getConfig().getDownloader();

    public File prepareDocument(PrintDocument printDocument) throws Exception {
        FileUtils.forceMkdir(new File(downloaderConfig.getPath()));

        if (printDocument.getUrl() == null && printDocument.getFileContent() == null) {
            throw new Exception("Both URL and File Content are null");
        }

        File output = getOutputFile(printDocument);
        if (printDocument.getFileContent() != null) {
            byte[] bytes = Base64.getDecoder().decode(printDocument.getFileContent());
            Files.write(output.toPath(), bytes);
        } else {
            URL url = new URL(printDocument.getUrl());
            download(url, getOutputFile(printDocument));
        }

        return output;
    }

    public void deleteDocument(PrintDocument printDocument) throws IOException {
        FileUtils.deleteQuietly(getOutputFile(printDocument));
    }

    private File getOutputFile(PrintDocument printDocument) throws MalformedURLException {
        File output;
        if (printDocument.getFileContent() != null) {
            output = new File(downloaderConfig.getPath() + "/" + printDocument.getUuid() + "-" + printDocument.getUrl());
        } else {
            URL url = new URL(printDocument.getUrl());
            output = new File(downloaderConfig.getPath() + "/" + printDocument.getUuid() + "-" + FilenameUtils.getName(url.getPath()));
        }
        return output;
    }

    private void download(URL url, File outputFile) throws Exception {
        log.info("Downloading file from: {}", url);

        long timeStart = System.currentTimeMillis();

        if (downloaderConfig.isIgnoreTLSCertificateError()) {
            TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        public X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }

                        public void checkClientTrusted(X509Certificate[] certs, String authType) {
                        }

                        public void checkServerTrusted(X509Certificate[] certs, String authType) {
                        }

                    }
            };

            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        }

        URLConnection urlConnection = url.openConnection();
        urlConnection.setConnectTimeout((int) downloaderConfig.getTimeout() * 1000);
        urlConnection.setReadTimeout((int) downloaderConfig.getTimeout() * 1000);
        urlConnection.connect();

        int contentLength = urlConnection.getContentLength();
        int responseCode;
        if (urlConnection instanceof HttpsURLConnection) {
            responseCode = ((HttpsURLConnection) urlConnection).getResponseCode();
        } else {
            responseCode = ((HttpURLConnection) urlConnection).getResponseCode();
        }

        log.trace("Content Length: {}", contentLength);
        log.trace("Response Code: {}", responseCode);

        // Status code mismatch
        if (responseCode != 200) {
            throw new IOException("HTTP Status Code: " + responseCode);
        }

        FileUtils.copyInputStreamToFile(urlConnection.getInputStream(), outputFile);

        long timeFinish = System.currentTimeMillis();
        log.info("File {} downloaded in {} ms", outputFile.getName(), timeFinish - timeStart);
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/AnnotatedPrintable.java
================================================
package tigerworkshop.webapphardwarebridge.utils;

import lombok.Data;
import lombok.extern.log4j.Log4j2;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.util.ArrayList;

@Log4j2
public class AnnotatedPrintable implements Printable {
    private final Printable printable;
    private final ArrayList<AnnotatedPrintableAnnotation> annotatedPrintableAnnotationArrayList = new ArrayList<>();

    private static final Double MM_TO_PPI = 2.8346457;

    public AnnotatedPrintable(Printable printable) {
        this.printable = printable;
    }

    public void addAnnotation(AnnotatedPrintableAnnotation annotatedPrintableAnnotation) {
        annotatedPrintableAnnotationArrayList.add(annotatedPrintableAnnotation);
    }

    @Override
    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
        int result = printable.print(graphics, pageFormat, pageIndex);

        if (annotatedPrintableAnnotationArrayList.isEmpty()) {
            return result;
        }

        if (result == PAGE_EXISTS) {
            Graphics2D graphics2D = (Graphics2D) graphics;

            // On Windows we need getDefaultTransform() to print in correct scale
            // But on Mac it causes NullPointerException, however a blank AffineTransform works
            try {
                graphics2D.setTransform(graphics2D.getDeviceConfiguration().getDefaultTransform());
            } catch (Exception e) {
                graphics2D.setTransform(new AffineTransform());
            }

            float clipX = (float) graphics2D.getClipBounds().getX();
            float clipY = (float) graphics2D.getClipBounds().getY();

            // Catch Exceptions otherwise blank page occur while exceptions silently handled
            try {
                for (AnnotatedPrintableAnnotation annotatedPrintableAnnotation : annotatedPrintableAnnotationArrayList) {
                    if (annotatedPrintableAnnotation.getText() == null) {
                        log.warn("annotatedPrintableAnnotation.getText() is null");
                        continue;
                    }

                    float realX = (float) (clipX + annotatedPrintableAnnotation.getX() * MM_TO_PPI);
                    float realY = (float) (clipY + annotatedPrintableAnnotation.getY() * MM_TO_PPI);

                    int isBold = annotatedPrintableAnnotation.getBold() != null ? Font.BOLD : Font.PLAIN;
                    int fontSize = annotatedPrintableAnnotation.getSize() != null ? annotatedPrintableAnnotation.getSize() : 10;

                    Font font = new Font("Sans-Serif", isBold, fontSize);
                    graphics2D.setColor(Color.BLACK);
                    graphics2D.setFont(font);
                    graphics2D.drawString(annotatedPrintableAnnotation.getText(), realX, realY);
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }

        }

        return result;
    }

    @Data
    public static class AnnotatedPrintableAnnotation {
        private String text;
        private Float x;
        private Float y;
        private Integer size;
        private Boolean bold;
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/CertificateGenerator.java
================================================
package tigerworkshop.webapphardwarebridge.utils;

import lombok.extern.log4j.Log4j2;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.regex.Pattern;

@Log4j2
public class CertificateGenerator {
    private static final String CERTIFICATE_ALGORITHM = "RSA";
    private static final String CERTIFICATE_ISSUER = "CN=127.0.0.1";
    private static final String CERTIFICATE_DOMAIN = "CN=127.0.0.1";
    private static final int CERTIFICATE_BITS = 2048;

    private static final String IPV4_REGEX = "(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))";
    private static final Pattern IPV4_PATTERN = Pattern.compile(IPV4_REGEX);

    public static void generateSelfSignedCertificate(String address, String certificatePath, String keyPath) {
        Security.addProvider(new BouncyCastleProvider());

        if (!isCertificateAndKeyExist(certificatePath, keyPath)) {
            try {
                log.info("Certificate or private key does not exist, attempt to generate.");

                KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM);
                keyPairGenerator.initialize(CERTIFICATE_BITS, new SecureRandom());
                KeyPair keyPair = keyPairGenerator.generateKeyPair();

                X500Name issuer = new X500Name(CERTIFICATE_ISSUER);
                X500Name subject = new X500Name(CERTIFICATE_DOMAIN);
                BigInteger serialNumber = new BigInteger(64, new SecureRandom());
                Date validFrom = new Date();
                Date validTo = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10));
                SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
                ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(new BouncyCastleProvider()).build(keyPair.getPrivate());

                X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(issuer, serialNumber, validFrom, validTo, subject, subPubKeyInfo);

                final GeneralNames subjectAltNames;
                if (IPV4_PATTERN.matcher(address).matches()) {
                    subjectAltNames = new GeneralNames(new GeneralName(GeneralName.iPAddress, address));
                } else {
                    subjectAltNames = new GeneralNames(new GeneralName(GeneralName.dNSName, address));
                }
                certificateBuilder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames);

                X509CertificateHolder certificateHolder = certificateBuilder.build(signer);
                X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certificateHolder);

                log.info("Certificate and private key generated.");

                File directory = new File("tls");
                if (!directory.isDirectory()) {
                    directory.mkdir();
                }

                saveCert(cert, certificatePath);
                saveKey(keyPair.getPrivate(), keyPath);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        } else {
            log.info("Certificate and private key already exists.");
        }
    }

    public static Boolean isCertificateAndKeyExist(String certificatePath, String keyPath) {
        File certificate = new File(certificatePath);
        File privateKey = new File(keyPath);

        return certificate.exists() && privateKey.exists();
    }

    private static void saveCert(X509Certificate cert, String certificatePath) {
        try {
            JcaPEMWriter writer = new JcaPEMWriter(new FileWriter(certificatePath));
            writer.writeObject(cert);
            writer.close();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    private static void saveKey(PrivateKey key, String keyPath) {
        try {
            JcaPEMWriter writer = new JcaPEMWriter(new FileWriter(keyPath));
            writer.writeObject(new JcaPKCS8Generator(key, null));
            writer.close();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/ImagePrintable.java
================================================
package tigerworkshop.webapphardwarebridge.utils;

import java.awt.*;
import java.awt.print.PageFormat;
import java.awt.print.Printable;

public class ImagePrintable implements Printable {
    private final Image image;

    public ImagePrintable(Image image) {
        this.image = image;
    }

    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) {
        if (pageIndex >= 1) {
            return Printable.NO_SUCH_PAGE;
        }

        Graphics2D g2d = (Graphics2D) graphics;
        g2d.translate((int) pageFormat.getImageableX(), (int) pageFormat.getImageableY());

        double width = pageFormat.getImageableWidth();
        double height = pageFormat.getImageableHeight();

        g2d.drawImage(image, 0, 0, (int) width, (int) height, null, null);

        return Printable.PAGE_EXISTS;
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/ThreadUtil.java
================================================
package tigerworkshop.webapphardwarebridge.utils;

public class ThreadUtil {
    public static void silentSleep(long duration) {
        try {
            Thread.sleep(duration);
        } catch (Exception ignored) {
        }
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/PrinterWebSocketService.java
================================================
package tigerworkshop.webapphardwarebridge.websocketservices;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FilenameUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.printing.PDFPrintable;
import org.apache.pdfbox.printing.Scaling;
import tigerworkshop.webapphardwarebridge.dtos.Config;
import tigerworkshop.webapphardwarebridge.dtos.NotificationDTO;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
import tigerworkshop.webapphardwarebridge.responses.PrintDocument;
import tigerworkshop.webapphardwarebridge.responses.PrintResult;
import tigerworkshop.webapphardwarebridge.services.ConfigService;
import tigerworkshop.webapphardwarebridge.services.DocumentService;
import tigerworkshop.webapphardwarebridge.utils.AnnotatedPrintable;
import tigerworkshop.webapphardwarebridge.utils.ImagePrintable;

import javax.imageio.ImageIO;
import javax.print.*;
import java.awt.*;
import java.awt.print.*;
import java.io.File;
import java.util.Optional;

@Log4j2
public class PrinterWebSocketService implements WebSocketServiceInterface {
    private WebSocketServerInterface server;

    private static final ConfigService configService = ConfigService.getInstance();
    private static final DocumentService documentService = DocumentService.getInstance();
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    public PrinterWebSocketService() {
        log.info("Starting PrinterWebSocketService");
    }

    @Override
    public void start() {

    }

    @Override
    public void stop() {

    }

    @Override
    public void messageToService(String message) {
        try {
            PrintDocument printDocument = objectMapper.readValue(message, PrintDocument.class);
            printDocument(printDocument);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    @Override
    public void messageToService(byte[] message) {
        log.error("PrinterWebSocketService onDataReceived: binary data not supported");
    }

    @Override
    public void onRegister(WebSocketServerInterface server) {
        this.server = server;
    }

    @Override
    public void onUnregister() {
        this.server = null;
    }

    @Override
    public String getChannel() {
        return "/printer";
    }

    /**
     * Prints a PrintDocument
     */
    public void printDocument(PrintDocument printDocument) throws Exception {
        log.info("Printing Document {}, {}", printDocument.getType(), printDocument.getUrl());

        PrinterSearchResult printerSearchResult = null;
        try {
            printerSearchResult = searchPrinterForType(printDocument.getType());

            server.messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("INFO", "Printing " + printDocument.getType(), printDocument.getUrl())));

            if (isRaw(printDocument)) {
                printRaw(printDocument, printerSearchResult);
            } else if (isImage(printDocument)) {
                printImage(printDocument, printerSearchResult);
            } else if (isPDF(printDocument)) {
                printPDF(printDocument, printerSearchResult);
            } else {
                throw new Exception("Unknown file type: " + printDocument.getUrl());
            }

            server.messageToServer(getChannel(), objectMapper.writeValueAsString(new PrintResult(true, "Success", printDocument.getId(), printerSearchResult.getName())));
        } catch (Exception e) {
            String errorMessage = e.getMessage();

            if (e instanceof PrinterAbortException) {
                errorMessage = "Printing aborted";
            }

            log.error("Print Error: {}, {}", e.getClass().getName(), errorMessage);

            if (!isRaw(printDocument)) {
                log.error("Print Error: Deleting downloaded document");
                documentService.deleteDocument(printDocument);
            }

            server.messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("ERROR", "Print Error " + printDocument.getType(), errorMessage)));

            server.messageToServer(getChannel(), objectMapper.writeValueAsString(new PrintResult(false, errorMessage, printDocument.getId(), printerSearchResult != null ? printerSearchResult.getName() : null)));
        }
    }

    /**
     * Return if PrintDocument is raw
     */
    private Boolean isRaw(PrintDocument printDocument) {
        return printDocument.getRawContent() != null && !printDocument.getRawContent().isEmpty();
    }

    /**
     * Return if PrintDocument is image
     */
    private Boolean isImage(PrintDocument printDocument) {
        String filename = FilenameUtils.getName(printDocument.getUrl());

        return filename.matches("^.*\\.(jpg|jpeg|png|gif)$");
    }

    /**
     * Return if PrintDocument is PDF
     */
    private Boolean isPDF(PrintDocument printDocument) {
        String filename = FilenameUtils.getName(printDocument.getUrl());

        return filename.matches("^.*\\.(pdf)$");
    }

    /**
     * Prints raw bytes to specified printer.
     */
    private void printRaw(PrintDocument printDocument, PrinterSearchResult printerSearchResult) throws PrintException {
        log.debug("printRaw::{}", printDocument);
        long timeStart = System.currentTimeMillis();

        byte[] bytes = Base64.decodeBase64(printDocument.getRawContent());

        DocPrintJob docPrintJob = printerSearchResult.getDocPrintJob();
        Doc doc = new SimpleDoc(bytes, DocFlavor.BYTE_ARRAY.AUTOSENSE, null);
        docPrintJob.print(doc, null);

        long timeFinish = System.currentTimeMillis();
        log.info("printRaw finished in {} ms", timeFinish - timeStart);
    }

    /**
     * Prints image to specified printer.
     */
    private void printImage(PrintDocument printDocument, PrinterSearchResult printerSearchResult) throws Exception {
        log.debug("printImage::{}", printDocument);

        File file = documentService.prepareDocument(printDocument);
        String path = file.getPath();
        String filename = file.getName();

        long timeStart = System.currentTimeMillis();

        PrinterJob job = PrinterJob.getPrinterJob();
        job.setPrintService(printerSearchResult.getDocPrintJob().getPrintService());

        PageFormat pageFormat = getPageFormat(job, printerSearchResult);

        Image image = ImageIO.read(new File(path));

        Book book = new Book();
        AnnotatedPrintable printable = new AnnotatedPrintable(new ImagePrintable(image));

        for (AnnotatedPrintable.AnnotatedPrintableAnnotation printDocumentExtra : printDocument.getExtras()) {
            printable.addAnnotation(printDocumentExtra);
        }

        book.append(printable, pageFormat);

        job.setPageable(book);
        job.setJobName(filename);
        job.setCopies(printDocument.getQty());
        job.print();

        long timeFinish = System.currentTimeMillis();

        log.info("printImage {} finished in {} ms", filename, timeFinish - timeStart);
    }

    /**
     * Prints PDF to specified printer.
     */
    private void printPDF(PrintDocument printDocument, PrinterSearchResult printerSearchResult) throws Exception {
        log.debug("printPDF::{}", printDocument);

        File file = documentService.prepareDocument(printDocument);
        String path = file.getPath();
        String filename = file.getName();

        long timeStart = System.currentTimeMillis();

        DocPrintJob docPrintJob = printerSearchResult.getDocPrintJob();

        PrinterJob job = PrinterJob.getPrinterJob();
        job.setPrintService(docPrintJob.getPrintService());

        PageFormat pageFormat = getPageFormat(job, printerSearchResult);

        try (PDDocument document = PDDocument.load(new File(path))) {
            Book book = new Book();
            for (int i = 0; i < document.getNumberOfPages(); i += 1) {
                // Rotate Page Automatically
                PageFormat eachPageFormat = (PageFormat) pageFormat.clone();

                if (printerSearchResult.getMapping().isAutoRotate()) {
                    if (document.getPage(i).getCropBox().getWidth() > document.getPage(i).getCropBox().getHeight()) {
                        log.debug("Auto rotation result: LANDSCAPE");
                        eachPageFormat.setOrientation(PageFormat.LANDSCAPE);
                    } else {
                        log.debug("Auto rotation result: PORTRAIT");
                        eachPageFormat.setOrientation(PageFormat.PORTRAIT);
                    }
                }

                PDFPrintable pdfPrintable = new PDFPrintable(document, Scaling.SHRINK_TO_FIT, false, printerSearchResult.getMapping().getForceDPI());

                // Annotate Printable
                AnnotatedPrintable annotatedPrintable = new AnnotatedPrintable(pdfPrintable);
                for (AnnotatedPrintable.AnnotatedPrintableAnnotation printDocumentExtra : printDocument.getExtras()) {
                    annotatedPrintable.addAnnotation(printDocumentExtra);
                }

                book.append(annotatedPrintable, eachPageFormat);
            }

            job.setPageable(book);
            job.setJobName(filename);
            job.setCopies(printDocument.getQty());
            job.print();

            long timeFinish = System.currentTimeMillis();

            log.info("printPDF {} finished in {} ms", path, timeFinish - timeStart);
        }
    }

    private PageFormat getPageFormat(PrinterJob job, PrinterSearchResult printerSearchResult) {
        final PageFormat pageFormat = job.defaultPage();

        log.debug("PageFormat Size: {} x {}", pageFormat.getWidth(), pageFormat.getHeight());
        log.debug("PageFormat Imageable Size:{} x {}, XY: {}, {}", pageFormat.getImageableWidth(), pageFormat.getImageableHeight(), pageFormat.getImageableX(), pageFormat.getImageableY());
        log.debug("Paper Size: {} x {}", pageFormat.getPaper().getWidth(), pageFormat.getPaper().getHeight());
        log.debug("Paper Imageable Size: {} x {}, XY: {}, {}", pageFormat.getPaper().getImageableWidth(), pageFormat.getPaper().getImageableHeight(), pageFormat.getPaper().getImageableX(), pageFormat.getPaper().getImageableY());

        // Reset Imageable Area
        if (printerSearchResult.getMapping().isResetImageableArea()) {
            log.debug("PageFormat reset enabled");
            Paper paper = pageFormat.getPaper();
            paper.setImageableArea(0, 0, paper.getWidth(), paper.getHeight());
            pageFormat.setPaper(paper);
        }

        log.debug("Final Paper Size: {} x {}", pageFormat.getPaper().getWidth(), pageFormat.getPaper().getHeight());
        log.debug("Final Paper Imageable Size: {} x {}, XY: {}, {}", pageFormat.getPaper().getImageableWidth(), pageFormat.getPaper().getImageableHeight(), pageFormat.getPaper().getImageableX(), pageFormat.getPaper().getImageableY());

        return pageFormat;
    }

    /**
     * Get PrinterSearchResult for specified type
     */
    private PrinterSearchResult searchPrinterForType(String type) throws PrinterException {
        Optional<Config.PrinterMapping> printerMappingOptional = configService.getConfig().getPrinter().getMappings().stream().filter(it -> it.getType().equals(type)).findFirst();

        if (printerMappingOptional.isPresent()) {
            Config.PrinterMapping printerMapping = printerMappingOptional.get();
            PrintService[] printServices = PrinterJob.lookupPrintServices();

            for (PrintService printService : printServices) {
                if (printService.getName().equalsIgnoreCase(printerMapping.getName())) {
                    log.info("Sending print job type: {} to printer: {}", type, printService.getName());

                    return new PrinterSearchResult(printService.getName(), printerMapping, printService.createPrintJob(), false);
                }
            }
        }

         if (configService.getConfig().getPrinter().isAutoAddUnknownType()) {
             // Add unknown type does not already exist
             if (configService.getConfig().getPrinter().getMappings().stream().noneMatch(it -> it.getType().equals(type))) {
                 configService.addPrintTypeToList(type);
             }
        }

         if (configService.getConfig().getPrinter().isFallbackToDefault()) {
             log.info("No mapped print job type: {}, falling back to default printer", type);

             PrintService printService = PrintServiceLookup.lookupDefaultPrintService();

             if (printService == null) {
                 throw new PrinterException("No default printer found");
             }

             return new PrinterSearchResult(printService.getName(), new Config.PrinterMapping(), printService.createPrintJob(), true);
        }

         throw new PrinterException("No matched printer: " + type);
    }

    @Getter
    @AllArgsConstructor
    private static class PrinterSearchResult {
        private String name;
        private Config.PrinterMapping mapping;
        private DocPrintJob docPrintJob;
        private Boolean isDefault;
    }
}


================================================
FILE: src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/SerialWebSocketService.java
================================================
package tigerworkshop.webapphardwarebridge.websocketservices;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fazecast.jSerialComm.SerialPort;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Hex;
import tigerworkshop.webapphardwarebridge.dtos.Config;
import tigerworkshop.webapphardwarebridge.dtos.NotificationDTO;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
import tigerworkshop.webapphardwarebridge.utils.ThreadUtil;

import java.nio.charset.Charset;
import java.util.Objects;

@Log4j2
public class SerialWebSocketService implements WebSocketServiceInterface {
    private WebSocketServerInterface server;

    private static final ObjectMapper objectMapper = new ObjectMapper();

    private final Config.SerialMapping mapping;
    private final SerialPort serialPort;
    private byte[] writeBuffer = {};

    private Thread readThread;
    private Thread writeThread;
    private Thread monitorThread;

    private Boolean isRunning = true;

    private static final String BINARY = "BINARY";

    public SerialWebSocketService(Config.SerialMapping newMapping) {
        log.info("Starting SerialWebSocketService on {}", newMapping.getName());

        this.mapping = newMapping;

        this.serialPort = SerialPort.getCommPort(newMapping.getName());

        if (mapping.getBaudRate() != null) serialPort.setBaudRate(mapping.getBaudRate());
        if (mapping.getNumDataBits() != null) serialPort.setNumDataBits(mapping.getNumDataBits());
        if (mapping.getNumStopBits() != null) serialPort.setNumStopBits(mapping.getNumStopBits());
        if (mapping.getParity() != null) serialPort.setParity(mapping.getParity());
    }

    @Override
    public void start() {
        isRunning = true;

        readThread = new Thread(() -> {
            log.debug("Serial Read Thread started for {}", mapping.getName());

            while (isRunning) {
                if (serialPort.isOpen()) {
                    int bytesAvailable = serialPort.bytesAvailable();
                    if (bytesAvailable == 0) {
                        // No data coming from COM portName
                        ThreadUtil.silentSleep(10);
                        continue;
                    } else if (bytesAvailable == -1) {
                        // Check if portName closed unexpected (e.g. Unplugged)
                        serialPort.closePort();

                        try {
                            server.messageToService("/notification", objectMapper.writeValueAsString(new NotificationDTO("WARNING", "Serial Port", "Serial " + mapping.getName() + "(" + mapping.getType() + ") unplugged")));
                        } catch (JsonProcessingException e) {
                            log.error("Failed to send notification: {}", e.getMessage());
                        }

                        log.warn("Serial {} unplugged", mapping.getName());

                        continue;
                    }

                    int bytesToRead = mapping.getReadMultipleBytes() ? bytesAvailable : 1;

                    byte[] receivedData = new byte[bytesToRead];
                    serialPort.readBytes(receivedData, bytesToRead);

                    if (server != null) {
                        if (Objects.equals(mapping.getReadCharset(), BINARY)) server.messageToServer(getChannel(), receivedData);
                        else server.messageToServer(getChannel(), new String(receivedData, Charset.forName(mapping.getReadCharset())));
                    }
                }
            }

            log.debug("Serial Read Thread stopped for {}", mapping.getName());
        });

        writeThread = new Thread(() -> {
            log.debug("Serial Write Thread started for {}", mapping.getName());

            while (isRunning) {
                if (serialPort.isOpen()) {
                    if (writeBuffer.length > 0) {
                        log.trace("Bytes: {}", Hex.encodeHexString(writeBuffer));

                        serialPort.writeBytes(writeBuffer, writeBuffer.length);
                        writeBuffer = new byte[]{};
                    }
                    ThreadUtil.silentSleep(10);
                }
            }

            log.debug("Serial Write Thread stopped for {}", mapping.getName());
        });

        monitorThread = new Thread(() -> {
            log.debug("Serial Monitor Thread started for {}", mapping.getName());

            while (isRunning) {
                if (serialPort.isOpen()) {
                    ThreadUtil.silentSleep(1000);
                } else {
                    log.info("Trying to connect to serial @ {}", serialPort.getSystemPortName());
                    serialPort.openPort(1000);

                    if (serialPort.isOpen()) {
                        log.info("Serial {} is now open", mapping.getName());
                    }
                }
            }

            log.debug("Serial Monitor Thread stopped for {}", mapping.getName());
        });

        readThread.start();
        writeThread.start();
        monitorThread.start();
    }

    @Override
    public void stop() {
        log.info("Stopping SerialWebSocketService");

        isRunning = false;

        readThread.interrupt();
        writeThread.interrupt();
        monitorThread.interrupt();

        serialPort.closePort();

        log.info("Stopped SerialWebSocketService");
    }

    @Override
    public void messageToService(String message) {
        messageToService(message.getBytes());
    }

    @Override
    public void messageToService(byte[] message) {
        writeBuffer = message;
    }

    @Override
    public void onRegister(WebSocketServerInterface newServer) {
        this.server = newServer;
    }

    @Override
    public void onUnregister() {
        this.server = null;
    }

    @Override
    public String getChannel() {
        return "/serial/" + mapping.getType();
    }
}


================================================
FILE: src/main/resources/META-INF/MANIFEST.MF
================================================
Manifest-Version: 1.0
Main-Class: tigerworkshop.webapphardwarebridge.GUI
Class-Path: commons-io-2.16.1.jar jetty-security-11.0.21.jar sslcontext-
 kickstart-for-pem-8.3.6.jar commons-logging-1.2.jar kotlin-stdlib-jdk7-
 1.9.24.jar kotlin-stdlib-jdk8-1.9.24.jar websocket-jetty-api-11.0.21.ja
 r jetty-xml-11.0.21.jar jetty-alpn-server-11.0.21.jar websocket-core-co
 mmon-11.0.21.jar log4j-core-2.23.1.jar bcpkix-jdk18on-1.78.1.jar websoc
 ket-servlet-11.0.21.jar log4j-api-2.23.1.jar javalin-6.2.0.jar websocke
 t-jetty-server-11.0.21.jar jetty-util-11.0.21.jar bcutil-jdk18on-1.78.1
 .jar websocket-jetty-common-11.0.21.jar pdfbox-2.0.31.jar jetty-alpn-co
 nscrypt-server-11.0.21.jar sslcontext-kickstart-8.3.6.jar http2-server-
 11.0.21.jar bcprov-jdk18on-1.78.1.jar jetty-io-11.0.21.jar kotlin-stdli
 b-1.9.24.jar jetty-webapp-11.0.21.jar websocket-core-server-11.0.21.jar
  jetty-jakarta-servlet-api-5.0.2.jar sslcontext-kickstart-for-jetty-8.3
 .6.jar jetty-server-11.0.21.jar commons-codec-1.17.1.jar jSerialComm-2.
 11.0.jar http2-common-11.0.21.jar jackson-annotations-2.17.2.jar conscr
 ypt-openjdk-uber-2.5.2.jar httpcore5-5.2.5.jar jackson-core-2.17.2.jar 
 slf4j-api-2.0.13.jar annotations-13.0.jar fontbox-2.0.31.jar http2-hpac
 k-11.0.21.jar ssl-plugin-6.2.0.jar jetty-alpn-java-server-11.0.21.jar j
 etty-servlet-11.0.21.jar log4j-slf4j2-impl-2.23.1.jar jackson-databind-
 2.17.2.jar jetty-http-11.0.21.jar



================================================
FILE: src/main/resources/log4j2.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="warn">
    <appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %msg%n"/>
        </Console>
        <RollingFile name="RollingFile" fileName="log/client.log" filePattern="log/client-%i.log">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36}.%M() @%L - %msg%n"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="5"/>
        </RollingFile>
    </appenders>

    <loggers>
        <root level="INFO">
            <appender-ref ref="Console" level="INFO"/>
            <appender-ref ref="RollingFile" level="INFO"/>
        </root>
    </loggers>
</configuration>

================================================
FILE: src/main/resources/web/css/bootstrap-grid.css
================================================
/*!
 * Bootstrap Grid v5.2.3 (https://getbootstrap.com/)
 * Copyright 2011-2022 The Bootstrap Authors
 * Copyright 2011-2022 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 */
:root {
    --bs-blue: #0d6efd;
    --bs-indigo: #6610f2;
    --bs-purple: #6f42c1;
    --bs-pink: #d63384;
    --bs-red: #dc3545;
    --bs-orange: #fd7e14;
    --bs-yellow: #ffc107;
    --bs-green: #198754;
    --bs-teal: #20c997;
    --bs-cyan: #0dcaf0;
    --bs-black: #000;
    --bs-white: #fff;
    --bs-gray: #6c757d;
    --bs-gray-dark: #343a40;
    --bs-gray-100: #f8f9fa;
    --bs-gray-200: #e9ecef;
    --bs-gray-300: #dee2e6;
    --bs-gray-400: #ced4da;
    --bs-gray-500: #adb5bd;
    --bs-gray-600: #6c757d;
    --bs-gray-700: #495057;
    --bs-gray-800: #343a40;
    --bs-gray-900: #212529;
    --bs-primary: #0d6efd;
    --bs-secondary: #6c757d;
    --bs-success: #198754;
    --bs-info: #0dcaf0;
    --bs-warning: #ffc107;
    --bs-danger: #dc3545;
    --bs-light: #f8f9fa;
    --bs-dark: #212529;
    --bs-primary-rgb: 13, 110, 253;
    --bs-secondary-rgb: 108, 117, 125;
    --bs-success-rgb: 25, 135, 84;
    --bs-info-rgb: 13, 202, 240;
    --bs-warning-rgb: 255, 193, 7;
    --bs-danger-rgb: 220, 53, 69;
    --bs-light-rgb: 248, 249, 250;
    --bs-dark-rgb: 33, 37, 41;
    --bs-white-rgb: 255, 255, 255;
    --bs-black-rgb: 0, 0, 0;
    --bs-body-color-rgb: 33, 37, 41;
    --bs-body-bg-rgb: 255, 255, 255;
    --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
    --bs-body-font-family: var(--bs-font-sans-serif);
    --bs-body-font-size: 1rem;
    --bs-body-font-weight: 400;
    --bs-body-line-height: 1.5;
    --bs-body-color: #212529;
    --bs-body-bg: #fff;
    --bs-border-width: 1px;
    --bs-border-style: solid;
    --bs-border-color: #dee2e6;
    --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
    --bs-border-radius: 0.375rem;
    --bs-border-radius-sm: 0.25rem;
    --bs-border-radius-lg: 0.5rem;
    --bs-border-radius-xl: 1rem;
    --bs-border-radius-2xl: 2rem;
    --bs-border-radius-pill: 50rem;
    --bs-link-color: #0d6efd;
    --bs-link-hover-color: #0a58ca;
    --bs-code-color: #d63384;
    --bs-highlight-bg: #fff3cd;
}

.container,
.container-fluid,
.container-xxl,
.container-xl,
.container-lg,
.container-md,
.container-sm {
    --bs-gutter-x: 1.5rem;
    --bs-gutter-y: 0;
    width: 100%;
    padding-right: calc(var(--bs-gutter-x) * 0.5);
    padding-left: calc(var(--bs-gutter-x) * 0.5);
    margin-right: auto;
    margin-left: auto;
}

@media (min-width: 576px) {
    .container-sm, .container {
        max-width: 540px;
    }
}

@media (min-width: 768px) {
    .container-md, .container-sm, .container {
        max-width: 720px;
    }
}

@media (min-width: 992px) {
    .container-lg, .container-md, .container-sm, .container {
        max-width: 960px;
    }
}

@media (min-width: 1200px) {
    .container-xl, .container-lg, .container-md, .container-sm, .container {
        max-width: 1140px;
    }
}

@media (min-width: 1400px) {
    .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
        max-width: 1320px;
    }
}

.row {
    --bs-gutter-x: 1.5rem;
    --bs-gutter-y: 0;
    display: flex;
    flex-wrap: wrap;
    margin-top: calc(-1 * var(--bs-gutter-y));
    margin-right: calc(-0.5 * var(--bs-gutter-x));
    margin-left: calc(-0.5 * var(--bs-gutter-x));
}

.row > * {
    box-sizing: border-box;
    flex-shrink: 0;
    width: 100%;
    max-width: 100%;
    padding-right: calc(var(--bs-gutter-x) * 0.5);
    padding-left: calc(var(--bs-gutter-x) * 0.5);
    margin-top: var(--bs-gutter-y);
}

.col {
    flex: 1 0 0%;
}

.row-cols-auto > * {
    flex: 0 0 auto;
    width: auto;
}

.row-cols-1 > * {
    flex: 0 0 auto;
    width: 100%;
}

.row-cols-2 > * {
    flex: 0 0 auto;
    width: 50%;
}

.row-cols-3 > * {
    flex: 0 0 auto;
    width: 33.3333333333%;
}

.row-cols-4 > * {
    flex: 0 0 auto;
    width: 25%;
}

.row-cols-5 > * {
    flex: 0 0 auto;
    width: 20%;
}

.row-cols-6 > * {
    flex: 0 0 auto;
    width: 16.6666666667%;
}

.col-auto {
    flex: 0 0 auto;
    width: auto;
}

.col-1 {
    flex: 0 0 auto;
    width: 8.33333333%;
}

.col-2 {
    flex: 0 0 auto;
    width: 16.66666667%;
}

.col-3 {
    flex: 0 0 auto;
    width: 25%;
}

.col-4 {
    flex: 0 0 auto;
    width: 33.33333333%;
}

.col-5 {
    flex: 0 0 auto;
    width: 41.66666667%;
}

.col-6 {
    flex: 0 0 auto;
    width: 50%;
}

.col-7 {
    flex: 0 0 auto;
    width: 58.33333333%;
}

.col-8 {
    flex: 0 0 auto;
    width: 66.66666667%;
}

.col-9 {
    flex: 0 0 auto;
    width: 75%;
}

.col-10 {
    flex: 0 0 auto;
    width: 83.33333333%;
}

.col-11 {
    flex: 0 0 auto;
    width: 91.66666667%;
}

.col-12 {
    flex: 0 0 auto;
    width: 100%;
}

.offset-1 {
    margin-left: 8.33333333%;
}

.offset-2 {
    margin-left: 16.66666667%;
}

.offset-3 {
    margin-left: 25%;
}

.offset-4 {
    margin-left: 33.33333333%;
}

.offset-5 {
    margin-left: 41.66666667%;
}

.offset-6 {
    margin-left: 50%;
}

.offset-7 {
    margin-left: 58.33333333%;
}

.offset-8 {
    margin-left: 66.66666667%;
}

.offset-9 {
    margin-left: 75%;
}

.offset-10 {
    margin-left: 83.33333333%;
}

.offset-11 {
    margin-left: 91.66666667%;
}

.g-0,
.gx-0 {
    --bs-gutter-x: 0;
}

.g-0,
.gy-0 {
    --bs-gutter-y: 0;
}

.g-1,
.gx-1 {
    --bs-gutter-x: 0.25rem;
}

.g-1,
.gy-1 {
    --bs-gutter-y: 0.25rem;
}

.g-2,
.gx-2 {
    --bs-gutter-x: 0.5rem;
}

.g-2,
.gy-2 {
    --bs-gutter-y: 0.5rem;
}

.g-3,
.gx-3 {
    --bs-gutter-x: 1rem;
}

.g-3,
.gy-3 {
    --bs-gutter-y: 1rem;
}

.g-4,
.gx-4 {
    --bs-gutter-x: 1.5rem;
}

.g-4,
.gy-4 {
    --bs-gutter-y: 1.5rem;
}

.g-5,
.gx-5 {
    --bs-gutter-x: 3rem;
}

.g-5,
.gy-5 {
    --bs-gutter-y: 3rem;
}

@media (min-width: 576px) {
    .col-sm {
        flex: 1 0 0%;
    }

    .row-cols-sm-auto > * {
        flex: 0 0 auto;
        width: auto;
    }

    .row-cols-sm-1 > * {
        flex: 0 0 auto;
        width: 100%;
    }

    .row-cols-sm-2 > * {
        flex: 0 0 auto;
        width: 50%;
    }

    .row-cols-sm-3 > * {
        flex: 0 0 auto;
        width: 33.3333333333%;
    }

    .row-cols-sm-4 > * {
        flex: 0 0 auto;
        width: 25%;
    }

    .row-cols-sm-5 > * {
        flex: 0 0 auto;
        width: 20%;
    }

    .row-cols-sm-6 > * {
        flex: 0 0 auto;
        width: 16.6666666667%;
    }

    .col-sm-auto {
        flex: 0 0 auto;
        width: auto;
    }

    .col-sm-1 {
        flex: 0 0 auto;
        width: 8.33333333%;
    }

    .col-sm-2 {
        flex: 0 0 auto;
        width: 16.66666667%;
    }

    .col-sm-3 {
        flex: 0 0 auto;
        width: 25%;
    }

    .col-sm-4 {
        flex: 0 0 auto;
        width: 33.33333333%;
    }

    .col-sm-5 {
        flex: 0 0 auto;
        width: 41.66666667%;
    }

    .col-sm-6 {
        flex: 0 0 auto;
        width: 50%;
    }

    .col-sm-7 {
        flex: 0 0 auto;
        width: 58.33333333%;
    }

    .col-sm-8 {
        flex: 0 0 auto;
        width: 66.66666667%;
    }

    .col-sm-9 {
        flex: 0 0 auto;
        width: 75%;
    }

    .col-sm-10 {
        flex: 0 0 auto;
        width: 83.33333333%;
    }

    .col-sm-11 {
        flex: 0 0 auto;
        width: 91.66666667%;
    }

    .col-sm-12 {
        flex: 0 0 auto;
        width: 100%;
    }

    .offset-sm-0 {
        margin-left: 0;
    }

    .offset-sm-1 {
        margin-left: 8.33333333%;
    }

    .offset-sm-2 {
        margin-left: 16.66666667%;
    }

    .offset-sm-3 {
        margin-left: 25%;
    }

    .offset-sm-4 {
        margin-left: 33.33333333%;
    }

    .offset-sm-5 {
        margin-left: 41.66666667%;
    }

    .offset-sm-6 {
        margin-left: 50%;
    }

    .offset-sm-7 {
        margin-left: 58.33333333%;
    }

    .offset-sm-8 {
        margin-left: 66.66666667%;
    }

    .offset-sm-9 {
        margin-left: 75%;
    }

    .offset-sm-10 {
        margin-left: 83.33333333%;
    }

    .offset-sm-11 {
        margin-left: 91.66666667%;
    }

    .g-sm-0,
    .gx-sm-0 {
        --bs-gutter-x: 0;
    }

    .g-sm-0,
    .gy-sm-0 {
        --bs-gutter-y: 0;
    }

    .g-sm-1,
    .gx-sm-1 {
        --bs-gutter-x: 0.25rem;
    }

    .g-sm-1,
    .gy-sm-1 {
        --bs-gutter-y: 0.25rem;
    }

    .g-sm-2,
    .gx-sm-2 {
        --bs-gutter-x: 0.5rem;
    }

    .g-sm-2,
    .gy-sm-2 {
        --bs-gutter-y: 0.5rem;
    }

    .g-sm-3,
    .gx-sm-3 {
        --bs-gutter-x: 1rem;
    }

    .g-sm-3,
    .gy-sm-3 {
        --bs-gutter-y: 1rem;
    }

    .g-sm-4,
    .gx-sm-4 {
        --bs-gutter-x: 1.5rem;
    }

    .g-sm-4,
    .gy-sm-4 {
        --bs-gutter-y: 1.5rem;
    }

    .g-sm-5,
    .gx-sm-5 {
        --bs-gutter-x: 3rem;
    }

    .g-sm-5,
    .gy-sm-5 {
        --bs-gutter-y: 3rem;
    }
}

@media (min-width: 768px) {
    .col-md {
        flex: 1 0 0%;
    }

    .row-cols-md-auto > * {
        flex: 0 0 auto;
        width: auto;
    }

    .row-cols-md-1 > * {
        flex: 0 0 auto;
        width: 100%;
    }

    .row-cols-md-2 > * {
        flex: 0 0 auto;
        width: 50%;
    }

    .row-cols-md-3 > * {
        flex: 0 0 auto;
        width: 33.3333333333%;
    }

    .row-cols-md-4 > * {
        flex: 0 0 auto;
        width: 25%;
    }

    .row-cols-md-5 > * {
        flex: 0 0 auto;
        width: 20%;
    }

    .row-cols-md-6 > * {
        flex: 0 0 auto;
        width: 16.6666666667%;
    }

    .col-md-auto {
        flex: 0 0 auto;
        width: auto;
    }

    .col-md-1 {
        flex: 0 0 auto;
        width: 8.33333333%;
    }

    .col-md-2 {
        flex: 0 0 auto;
        width: 16.66666667%;
    }

    .col-md-3 {
        flex: 0 0 auto;
        width: 25%;
    }

    .col-md-4 {
        flex: 0 0 auto;
        width: 33.33333333%;
    }

    .col-md-5 {
        flex: 0 0 auto;
        width: 41.66666667%;
    }

    .col-md-6 {
        flex: 0 0 auto;
        width: 50%;
    }

    .col-md-7 {
        flex: 0 0 auto;
        width: 58.33333333%;
    }

    .col-md-8 {
        flex: 0 0 auto;
        width: 66.66666667%;
    }

    .col-md-9 {
        flex: 0 0 auto;
        width: 75%;
    }

    .col-md-10 {
        flex: 0 0 auto;
        width: 83.33333333%;
    }

    .col-md-11 {
        flex: 0 0 auto;
        width: 91.66666667%;
    }

    .col-md-12 {
        flex: 0 0 auto;
        width: 100%;
    }

    .offset-md-0 {
        margin-left: 0;
    }

    .offset-md-1 {
        margin-left: 8.33333333%;
    }

    .offset-md-2 {
        margin-left: 16.66666667%;
    }

    .offset-md-3 {
        margin-left: 25%;
    }

    .offset-md-4 {
        margin-left: 33.33333333%;
    }

    .offset-md-5 {
        margin-left: 41.66666667%;
    }

    .offset-md-6 {
        margin-left: 50%;
    }

    .offset-md-7 {
        margin-left: 58.33333333%;
    }

    .offset-md-8 {
        margin-left: 66.66666667%;
    }

    .offset-md-9 {
        margin-left: 75%;
    }

    .offset-md-10 {
        margin-left: 83.33333333%;
    }

    .offset-md-11 {
        margin-left: 91.66666667%;
    }

    .g-md-0,
    .gx-md-0 {
        --bs-gutter-x: 0;
    }

    .g-md-0,
    .gy-md-0 {
        --bs-gutter-y: 0;
    }

    .g-md-1,
    .gx-md-1 {
        --bs-gutter-x: 0.25rem;
    }

    .g-md-1,
    .gy-md-1 {
        --bs-gutter-y: 0.25rem;
    }

    .g-md-2,
    .gx-md-2 {
        --bs-gutter-x: 0.5rem;
    }

    .g-md-2,
    .gy-md-2 {
        --bs-gutter-y: 0.5rem;
    }

    .g-md-3,
    .gx-md-3 {
        --bs-gutter-x: 1rem;
    }

    .g-md-3,
    .gy-md-3 {
        --bs-gutter-y: 1rem;
    }

    .g-md-4,
    .gx-md-4 {
        --bs-gutter-x: 1.5rem;
    }

    .g-md-4,
    .gy-md-4 {
        --bs-gutter-y: 1.5rem;
    }

    .g-md-5,
    .gx-md-5 {
        --bs-gutter-x: 3rem;
    }

    .g-md-5,
    .gy-md-5 {
        --bs-gutter-y: 3rem;
    }
}

@media (min-width: 992px) {
    .col-lg {
        flex: 1 0 0%;
    }

    .row-cols-lg-auto > * {
        flex: 0 0 auto;
        width: auto;
    }

    .row-cols-lg-1 > * {
        flex: 0 0 auto;
        width: 100%;
    }

    .row-cols-lg-2 > * {
        flex: 0 0 auto;
        width: 50%;
    }

    .row-cols-lg-3 > * {
        flex: 0 0 auto;
        width: 33.3333333333%;
    }

    .row-cols-lg-4 > * {
        flex: 0 0 auto;
        width: 25%;
    }

    .row-cols-lg-5 > * {
        flex: 0 0 auto;
        width: 20%;
    }

    .row-cols-lg-6 > * {
        flex: 0 0 auto;
        width: 16.6666666667%;
    }

    .col-lg-auto {
        flex: 0 0 auto;
        width: auto;
    }

    .col-lg-1 {
        flex: 0 0 auto;
        width: 8.33333333%;
    }

    .col-lg-2 {
        flex: 0 0 auto;
        width: 16.66666667%;
    }

    .col-lg-3 {
        flex: 0 0 auto;
        width: 25%;
    }

    .col-lg-4 {
        flex: 0 0 auto;
        width: 33.33333333%;
    }

    .col-lg-5 {
        flex: 0 0 auto;
        width: 41.66666667%;
    }

    .col-lg-6 {
        flex: 0 0 auto;
        width: 50%;
    }

    .col-lg-7 {
        flex: 0 0 auto;
        width: 58.33333333%;
    }

    .col-lg-8 {
        flex: 0 0 auto;
        width: 66.66666667%;
    }

    .col-lg-9 {
        flex: 0 0 auto;
        width: 75%;
    }

    .col-lg-10 {
        flex: 0 0 auto;
        width: 83.33333333%;
    }

    .col-lg-11 {
        flex: 0 0 auto;
        width: 91.66666667%;
    }

    .col-lg-12 {
        flex: 0 0 auto;
        width: 100%;
    }

    .offset-lg-0 {
        margin-left: 0;
    }

    .offset-lg-1 {
        margin-left: 8.33333333%;
    }

    .offset-lg-2 {
        margin-left: 16.66666667%;
    }

    .offset-lg-3 {
        margin-left: 25%;
    }

    .offset-lg-4 {
        margin-left: 33.33333333%;
    }

    .offset-lg-5 {
        margin-left: 41.66666667%;
    }

    .offset-lg-6 {
        margin-left: 50%;
    }

    .offset-lg-7 {
        margin-left: 58.33333333%;
    }

    .offset-lg-8 {
        margin-left: 66.66666667%;
    }

    .offset-lg-9 {
        margin-left: 75%;
    }

    .offset-lg-10 {
        margin-left: 83.33333333%;
    }

    .offset-lg-11 {
        margin-left: 91.66666667%;
    }

    .g-lg-0,
    .gx-lg-0 {
        --bs-gutter-x: 0;
    }

    .g-lg-0,
    .gy-lg-0 {
        --bs-gutter-y: 0;
    }

    .g-lg-1,
    .gx-lg-1 {
        --bs-gutter-x: 0.25rem;
    }

    .g-lg-1,
    .gy-lg-1 {
        --bs-gutter-y: 0.25rem;
    }

    .g-lg-2,
    .gx-lg-2 {
        --bs-gutter-x: 0.5rem;
    }

    .g-lg-2,
    .gy-lg-2 {
        --bs-gutter-y: 0.5rem;
    }

    .g-lg-3,
    .gx-lg-3 {
        --bs-gutter-x: 1rem;
    }

    .g-lg-3,
    .gy-lg-3 {
        --bs-gutter-y: 1rem;
    }

    .g-lg-4,
    .gx-lg-4 {
        --bs-gutter-x: 1.5rem;
    }

    .g-lg-4,
    .gy-lg-4 {
        --bs-gutter-y: 1.5rem;
    }

    .g-lg-5,
    .gx-lg-5 {
        --bs-gutter-x: 3rem;
    }

    .g-lg-5,
    .gy-lg-5 {
        --bs-gutter-y: 3rem;
    }
}

@media (min-width: 1200px) {
    .col-xl {
        flex: 1 0 0%;
    }

    .row-cols-xl-auto > * {
        flex: 0 0 auto;
        width: auto;
    }

    .row-cols-xl-1 > * {
        flex: 0 0 auto;
        width: 100%;
    }

    .row-cols-xl-2 > * {
        flex: 0 0 auto;
        width: 50%;
    }

    .row-cols-xl-3 > * {
        flex: 0 0 auto;
        width: 33.3333333333%;
    }

    .row-cols-xl-4 > * {
        flex: 0 0 auto;
        width: 25%;
    }

    .row-cols-xl-5 > * {
        flex: 0 0 auto;
        width: 20%;
    }

    .row-cols-xl-6 > * {
        flex: 0 0 auto;
        width: 16.6666666667%;
    }

    .col-xl-auto {
        flex: 0 0 auto;
        width: auto;
    }

    .col-xl-1 {
        flex: 0 0 auto;
        width: 8.33333333%;
    }

    .col-xl-2 {
        flex: 0 0 auto;
        width: 16.66666667%;
    }

    .col-xl-3 {
        flex: 0 0 auto;
        width: 25%;
    }

    .col-xl-4 {
        flex: 0 0 auto;
        width: 33.33333333%;
    }

    .col-xl-5 {
        flex: 0 0 auto;
        width: 41.66666667%;
    }

    .col-xl-6 {
        flex: 0 0 auto;
        width: 50%;
    }

    .col-xl-7 {
        flex: 0 0 auto;
        width: 58.33333333%;
    }

    .col-xl-8 {
        flex: 0 0 auto;
        width: 66.66666667%;
    }

    .col-xl-9 {
        flex: 0 0 auto;
        width: 75%;
    }

    .col-xl-10 {
        flex: 0 0 auto;
        width: 83.33333333%;
    }

    .col-xl-11 {
        flex: 0 0 auto;
        width: 91.66666667%;
    }

    .col-xl-12 {
        flex: 0 0 auto;
        width: 100%;
    }

    .offset-xl-0 {
        margin-left: 0;
    }

    .offset-xl-1 {
        margin-left: 8.33333333%;
    }

    .offset-xl-2 {
        margin-left: 16.66666667%;
    }

    .offset-xl-3 {
        margin-left: 25%;
    }

    .offset-xl-4 {
        margin-left: 33.33333333%;
    }

    .offset-xl-5 {
        margin-left: 41.66666667%;
    }

    .offset-xl-6 {
        margin-left: 50%;
    }

    .offset-xl-7 {
        margin-left: 58.33333333%;
    }

    .offset-xl-8 {
        margin-left: 66.66666667%;
    }

    .offset-xl-9 {
        margin-left: 75%;
    }

    .offset-xl-10 {
        margin-left: 83.33333333%;
    }

    .offset-xl-11 {
        margin-left: 91.66666667%;
    }

    .g-xl-0,
    .gx-xl-0 {
        --bs-gutter-x: 0;
    }

    .g-xl-0,
    .gy-xl-0 {
        --bs-gutter-y: 0;
    }

    .g-xl-1,
    .gx-xl-1 {
        --bs-gutter-x: 0.25rem;
    }

    .g-xl-1,
    .gy-xl-1 {
        --bs-gutter-y: 0.25rem;
    }

    .g-xl-2,
    .gx-xl-2 {
        --bs-gutter-x: 0.5rem;
    }

    .g-xl-2,
    .gy-xl-2 {
        --bs-gutter-y: 0.5rem;
    }

    .g-xl-3,
    .gx-xl-3 {
        --bs-gutter-x: 1rem;
    }

    .g-xl-3,
    .gy-xl-3 {
        --bs-gutter-y: 1rem;
    }

    .g-xl-4,
    .gx-xl-4 {
        --bs-gutter-x: 1.5rem;
    }

    .g-xl-4,
    .gy-xl-4 {
        --bs-gutter-y: 1.5rem;
    }

    .g-xl-5,
    .gx-xl-5 {
        --bs-gutter-x: 3rem;
    }

    .g-xl-5,
    .gy-xl-5 {
        --bs-gutter-y: 3rem;
    }
}

@media (min-width: 1400px) {
    .col-xxl {
        flex: 1 0 0%;
    }

    .row-cols-xxl-auto > * {
        flex: 0 0 auto;
        width: auto;
    }

    .row-cols-xxl-1 > * {
        flex: 0 0 auto;
        width: 100%;
    }

    .row-cols-xxl-2 > * {
        flex: 0 0 auto;
        width: 50%;
    }

    .row-cols-xxl-3 > * {
        flex: 0 0 auto;
        width: 33.3333333333%;
    }

    .row-cols-xxl-4 > * {
        flex: 0 0 auto;
        width: 25%;
    }

    .row-cols-xxl-5 > * {
        flex: 0 0 auto;
        width: 20%;
    }

    .row-cols-xxl-6 > * {
        flex: 0 0 auto;
        width: 16.6666666667%;
    }

    .col-xxl-auto {
        flex: 0 0 auto;
        width: auto;
    }

    .col-xxl-1 {
        flex: 0 0 auto;
        width: 8.33333333%;
    }

    .col-xxl-2 {
        flex: 0 0 auto;
        width: 16.66666667%;
    }

    .col-xxl-3 {
        flex: 0 0 auto;
        width: 25%;
    }

    .col-xxl-4 {
        flex: 0 0 auto;
        width: 33.33333333%;
    }

    .col-xxl-5 {
        flex: 0 0 auto;
        width: 41.66666667%;
    }

    .col-xxl-6 {
        flex: 0 0 auto;
        width: 50%;
    }

    .col-xxl-7 {
        flex: 0 0 auto;
        width: 58.33333333%;
    }

    .col-xxl-8 {
        flex: 0 0 auto;
        width: 66.66666667%;
    }

    .col-xxl-9 {
        flex: 0 0 auto;
        width: 75%;
    }

    .col-xxl-10 {
        flex: 0 0 auto;
        width: 83.33333333%;
    }

    .col-xxl-11 {
        flex: 0 0 auto;
        width: 91.66666667%;
    }

    .col-xxl-12 {
        flex: 0 0 auto;
        width: 100%;
    }

    .offset-xxl-0 {
        margin-left: 0;
    }

    .offset-xxl-1 {
        margin-left: 8.33333333%;
    }

    .offset-xxl-2 {
        margin-left: 16.66666667%;
    }

    .offset-xxl-3 {
        margin-left: 25%;
    }

    .offset-xxl-4 {
        margin-left: 33.33333333%;
    }

    .offset-xxl-5 {
        margin-left: 41.66666667%;
    }

    .offset-xxl-6 {
        margin-left: 50%;
    }

    .offset-xxl-7 {
        margin-left: 58.33333333%;
    }

    .offset-xxl-8 {
        margin-left: 66.66666667%;
    }

    .offset-xxl-9 {
        margin-left: 75%;
    }

    .offset-xxl-10 {
        margin-left: 83.33333333%;
    }

    .offset-xxl-11 {
        margin-left: 91.66666667%;
    }

    .g-xxl-0,
    .gx-xxl-0 {
        --bs-gutter-x: 0;
    }

    .g-xxl-0,
    .gy-xxl-0 {
        --bs-gutter-y: 0;
    }

    .g-xxl-1,
    .gx-xxl-1 {
        --bs-gutter-x: 0.25rem;
    }

    .g-xxl-1,
    .gy-xxl-1 {
        --bs-gutter-y: 0.25rem;
    }

    .g-xxl-2,
    .gx-xxl-2 {
        --bs-gutter-x: 0.5rem;
    }

    .g-xxl-2,
    .gy-xxl-2 {
        --bs-gutter-y: 0.5rem;
    }

    .g-xxl-3,
    .gx-xxl-3 {
        --bs-gutter-x: 1rem;
    }

    .g-xxl-3,
    .gy-xxl-3 {
        --bs-gutter-y: 1rem;
    }

    .g-xxl-4,
    .gx-xxl-4 {
        --bs-gutter-x: 1.5rem;
    }

    .g-xxl-4,
    .gy-xxl-4 {
        --bs-gutter-y: 1.5rem;
    }

    .g-xxl-5,
    .gx-xxl-5 {
        --bs-gutter-x: 3rem;
    }

    .g-xxl-5,
    .gy-xxl-5 {
        --bs-gutter-y: 3rem;
    }
}

.d-inline {
    display: inline !important;
}

.d-inline-block {
    display: inline-block !important;
}

.d-block {
    display: block !important;
}

.d-grid {
    display: grid !important;
}

.d-table {
    display: table !important;
}

.d-table-row {
    display: table-row !important;
}

.d-table-cell {
    display: table-cell !important;
}

.d-flex {
    display: flex !important;
}

.d-inline-flex {
    display: inline-flex !important;
}

.d-none {
    display: none !important;
}

.flex-fill {
    flex: 1 1 auto !important;
}

.flex-row {
    flex-direction: row !important;
}

.flex-column {
    flex-direction: column !important;
}

.flex-row-reverse {
    flex-direction: row-reverse !important;
}

.flex-column-reverse {
    flex-direction: column-reverse !important;
}

.flex-grow-0 {
    flex-grow: 0 !important;
}

.flex-grow-1 {
    flex-grow: 1 !important;
}

.flex-shrink-0 {
    flex-shrink: 0 !important;
}

.flex-shrink-1 {
    flex-shrink: 1 !important;
}

.flex-wrap {
    flex-wrap: wrap !important;
}

.flex-nowrap {
    flex-wrap: nowrap !important;
}

.flex-wrap-reverse {
    flex-wrap: wrap-reverse !important;
}

.justify-content-start {
    justify-content: flex-start !important;
}

.justify-content-end {
    justify-content: flex-end !important;
}

.justify-content-center {
    justify-content: center !important;
}

.justify-content-between {
    justify-content: space-between !important;
}

.justify-content-around {
    justify-content: space-around !important;
}

.justify-content-evenly {
    justify-content: space-evenly !important;
}

.align-items-start {
    align-items: flex-start !important;
}

.align-items-end {
    align-items: flex-end !important;
}

.align-items-center {
    align-items: center !important;
}

.align-items-baseline {
    align-items: baseline !important;
}

.align-items-stretch {
    align-items: stretch !important;
}

.align-content-start {
    align-content: flex-start !important;
}

.align-content-end {
    align-content: flex-end !important;
}

.align-content-center {
    align-content: center !important;
}

.align-content-between {
    align-content: space-between !important;
}

.align-content-around {
    align-content: space-around !important;
}

.align-content-stretch {
    align-content: stretch !important;
}

.align-self-auto {
    align-self: auto !important;
}

.align-self-start {
    align-self: flex-start !important;
}

.align-self-end {
    align-self: flex-end !important;
}

.align-self-center {
    align-self: center !important;
}

.align-self-baseline {
    align-self: baseline !important;
}

.align-self-stretch {
    align-self: stretch !important;
}

.order-first {
    order: -1 !important;
}

.order-0 {
    order: 0 !important;
}

.order-1 {
    order: 1 !important;
}

.order-2 {
    order: 2 !important;
}

.order-3 {
    order: 3 !important;
}

.order-4 {
    order: 4 !important;
}

.order-5 {
    order: 5 !important;
}

.order-last {
    order: 6 !important;
}

.m-0 {
    margin: 0 !important;
}

.m-1 {
    margin: 0.25rem !important;
}

.m-2 {
    margin: 0.5rem !important;
}

.m-3 {
    margin: 1rem !important;
}

.m-4 {
    margin: 1.5rem !important;
}

.m-5 {
    margin: 3rem !important;
}

.m-auto {
    margin: auto !important;
}

.mx-0 {
    margin-right: 0 !important;
    margin-left: 0 !important;
}

.mx-1 {
    margin-right: 0.25rem !important;
    margin-left: 0.25rem !important;
}

.mx-2 {
    margin-right: 0.5rem !important;
    margin-left: 0.5rem !important;
}

.mx-3 {
    margin-right: 1rem !important;
    margin-left: 1rem !important;
}

.mx-4 {
    margin-right: 1.5rem !important;
    margin-left: 1.5rem !important;
}

.mx-5 {
    margin-right: 3rem !important;
    margin-left: 3rem !important;
}

.mx-auto {
    margin-right: auto !important;
    margin-left: auto !important;
}

.my-0 {
    margin-top: 0 !important;
    margin-bottom: 0 !important;
}

.my-1 {
    margin-top: 0.25rem !important;
    margin-bottom: 0.25rem !important;
}

.my-2 {
    margin-top: 0.5rem !important;
    margin-bottom: 0.5rem !important;
}

.my-3 {
    margin-top: 1rem !important;
    margin-bottom: 1rem !important;
}

.my-4 {
    margin-top: 1.5rem !important;
    margin-bottom: 1.5rem !important;
}

.my-5 {
    margin-top: 3rem !important;
    margin-bottom: 3rem !important;
}

.my-auto {
    margin-top: auto !important;
    margin-bottom: auto !important;
}

.mt-0 {
    margin-top: 0 !important;
}

.mt-1 {
    margin-top: 0.25rem !important;
}

.mt-2 {
    margin-top: 0.5rem !important;
}

.mt-3 {
    margin-top: 1rem !important;
}

.mt-4 {
    margin-top: 1.5rem !important;
}

.mt-5 {
    margin-top: 3rem !important;
}

.mt-auto {
    margin-top: auto !important;
}

.me-0 {
    margin-right: 0 !important;
}

.me-1 {
    margin-right: 0.25rem !important;
}

.me-2 {
    margin-right: 0.5rem !important;
}

.me-3 {
    margin-right: 1rem !important;
}

.me-4 {
    margin-right: 1.5rem !important;
}

.me-5 {
    margin-right: 3rem !important;
}

.me-auto {
    margin-right: auto !important;
}

.mb-0 {
    margin-bottom: 0 !important;
}

.mb-1 {
    margin-bottom: 0.25rem !important;
}

.mb-2 {
    margin-bottom: 0.5rem !important;
}

.mb-3 {
    margin-bottom: 1rem !important;
}

.mb-4 {
    margin-bottom: 1.5rem !important;
}

.mb-5 {
    margin-bottom: 3rem !important;
}

.mb-auto {
    margin-bottom: auto !important;
}

.ms-0 {
    margin-left: 0 !important;
}

.ms-1 {
    margin-left: 0.25rem !important;
}

.ms-2 {
    margin-left: 0.5rem !important;
}

.ms-3 {
    margin-left: 1rem !important;
}

.ms-4 {
    margin-left: 1.5rem !important;
}

.ms-5 {
    margin-left: 3rem !important;
}

.ms-auto {
    margin-left: auto !important;
}

.p-0 {
    padding: 0 !important;
}

.p-1 {
    padding: 0.25rem !important;
}

.p-2 {
    padding: 0.5rem !important;
}

.p-3 {
    padding: 1rem !important;
}

.p-4 {
    padding: 1.5rem !important;
}

.p-5 {
    padding: 3rem !important;
}

.px-0 {
    padding-right: 0 !important;
    padding-left: 0 !important;
}

.px-1 {
    padding-right: 0.25rem !important;
    padding-left: 0.25rem !important;
}

.px-2 {
    padding-right: 0.5rem !important;
    padding-left: 0.5rem !important;
}

.px-3 {
    padding-right: 1rem !important;
    padding-left: 1rem !important;
}

.px-4 {
    padding-right: 1.5rem !important;
    padding-left: 1.5rem !important;
}

.px-5 {
    padding-right: 3rem !important;
    padding-left: 3rem !important;
}

.py-0 {
    padding-top: 0 !important;
    padding-bottom: 0 !important;
}

.py-1 {
    padding-top: 0.25rem !important;
    padding-bottom: 0.25rem !important;
}

.py-2 {
    padding-top: 0.5rem !important;
    padding-bottom: 0.5rem !important;
}

.py-3 {
    padding-top: 1rem !important;
    padding-bottom: 1rem !important;
}

.py-4 {
    padding-top: 1.5rem !important;
    padding-bottom: 1.5rem !important;
}

.py-5 {
    padding-top: 3rem !important;
    padding-bottom: 3rem !important;
}

.pt-0 {
    padding-top: 0 !important;
}

.pt-1 {
    padding-top: 0.25rem !important;
}

.pt-2 {
    padding-top: 0.5rem !important;
}

.pt-3 {
    padding-top: 1rem !important;
}

.pt-4 {
    padding-top: 1.5rem !important;
}

.pt-5 {
    padding-top: 3rem !important;
}

.pe-0 {
    padding-right: 0 !important;
}

.pe-1 {
    padding-right: 0.25rem !important;
}

.pe-2 {
    padding-right: 0.5rem !important;
}

.pe-3 {
    padding-right: 1rem !important;
}

.pe-4 {
    padding-right: 1.5rem !important;
}

.pe-5 {
    padding-right: 3rem !important;
}

.pb-0 {
    padding-bottom: 0 !important;
}

.pb-1 {
    padding-bottom: 0.25rem !important;
}

.pb-2 {
    padding-bottom: 0.5rem !important;
}

.pb-3 {
    padding-bottom: 1rem !important;
}

.pb-4 {
    padding-bottom: 1.5rem !important;
}

.pb-5 {
    padding-bottom: 3rem !important;
}

.ps-0 {
    padding-left: 0 !important;
}

.ps-1 {
    padding-left: 0.25rem !important;
}

.ps-2 {
    padding-left: 0.5rem !important;
}

.ps-3 {
    padding-left: 1rem !important;
}

.ps-4 {
    padding-left: 1.5rem !important;
}

.ps-5 {
    padding-left: 3rem !important;
}

@media (min-width: 576px) {
    .d-sm-inline {
        display: inline !important;
    }

    .d-sm-inline-block {
        display: inline-block !important;
    }

    .d-sm-block {
        display: block !important;
    }

    .d-sm-grid {
        display: grid !important;
    }

    .d-sm-table {
        display: table !important;
    }

    .d-sm-table-row {
        display: table-row !important;
    }

    .d-sm-table-cell {
        display: table-cell !important;
    }

    .d-sm-flex {
        display: flex !important;
    }

    .d-sm-inline-flex {
        display: inline-flex !important;
    }

    .d-sm-none {
        display: none !important;
    }

    .flex-sm-fill {
        flex: 1 1 auto !important;
    }

    .flex-sm-row {
        flex-direction: row !important;
    }

    .flex-sm-column {
        flex-direction: column !important;
    }

    .flex-sm-row-reverse {
        flex-direction: row-reverse !important;
    }

    .flex-sm-column-reverse {
        flex-direction: column-reverse !important;
    }

    .flex-sm-grow-0 {
        flex-grow: 0 !important;
    }

    .flex-sm-grow-1 {
        flex-grow: 1 !important;
    }

    .flex-sm-shrink-0 {
        flex-shrink: 0 !important;
    }

    .flex-sm-shrink-1 {
        flex-shrink: 1 !important;
    }

    .flex-sm-wrap {
        flex-wrap: wrap !important;
    }

    .flex-sm-nowrap {
        flex-wrap: nowrap !important;
    }

    .flex-sm-wrap-reverse {
        flex-wrap: wrap-reverse !important;
    }

    .justify-content-sm-start {
        justify-content: flex-start !important;
    }

    .justify-content-sm-end {
        justify-content: flex-end !important;
    }

    .justify-content-sm-center {
        justify-content: center !important;
    }

    .justify-content-sm-between {
        justify-content: space-between !important;
    }

    .justify-content-sm-around {
        justify-content: space-around !important;
    }

    .justify-content-sm-evenly {
        justify-content: space-evenly !important;
    }

    .align-items-sm-start {
        align-items: flex-start !important;
    }

    .align-items-sm-end {
        align-items: flex-end !important;
    }

    .align-items-sm-center {
        align-items: center !important;
    }

    .align-items-sm-baseline {
        align-items: baseline !important;
    }

    .align-items-sm-stretch {
        align-items: stretch !important;
    }

    .align-content-sm-start {
        align-content: flex-start !important;
    }

    .align-content-sm-end {
        align-content: flex-end !important;
    }

    .align-content-sm-center {
        align-content: center !important;
    }

    .align-content-sm-between {
        align-content: space-between !important;
    }

    .align-content-sm-around {
        align-content: space-around !important;
    }

    .align-content-sm-stretch {
        align-content: stretch !important;
    }

    .align-self-sm-auto {
        align-self: auto !important;
    }

    .align-self-sm-start {
        align-self: flex-start !important;
    }

    .align-self-sm-end {
        align-self: flex-end !important;
    }

    .align-self-sm-center {
        align-self: center !important;
    }

    .align-self-sm-baseline {
        align-self: baseline !important;
    }

    .align-self-sm-stretch {
        align-self: stretch !important;
    }

    .order-sm-first {
        order: -1 !important;
    }

    .order-sm-0 {
        order: 0 !important;
    }

    .order-sm-1 {
        order: 1 !important;
    }

    .order-sm-2 {
        order: 2 !important;
    }

    .order-sm-3 {
        order: 3 !important;
    }

    .order-sm-4 {
        order: 4 !important;
    }

    .order-sm-5 {
        order: 5 !important;
    }

    .order-sm-last {
        order: 6 !important;
    }

    .m-sm-0 {
        margin: 0 !important;
    }

    .m-sm-1 {
        margin: 0.25rem !important;
    }

    .m-sm-2 {
        margin: 0.5rem !important;
    }

    .m-sm-3 {
        margin: 1rem !important;
    }

    .m-sm-4 {
        margin: 1.5rem !important;
    }

    .m-sm-5 {
        margin: 3rem !important;
    }

    .m-sm-auto {
        margin: auto !important;
    }

    .mx-sm-0 {
        margin-right: 0 !important;
        margin-left: 0 !important;
    }

    .mx-sm-1 {
        margin-right: 0.25rem !important;
        margin-left: 0.25rem !important;
    }

    .mx-sm-2 {
        margin-right: 0.5rem !important;
        margin-left: 0.5rem !important;
    }

    .mx-sm-3 {
        margin-right: 1rem !important;
        margin-left: 1rem !important;
    }

    .mx-sm-4 {
        margin-right: 1.5rem !important;
        margin-left: 1.5rem !important;
    }

    .mx-sm-5 {
        margin-right: 3rem !important;
        margin-left: 3rem !important;
    }

    .mx-sm-auto {
        margin-right: auto !important;
        margin-left: auto !important;
    }

    .my-sm-0 {
        margin-top: 0 !important;
        margin-bottom: 0 !important;
    }

    .my-sm-1 {
        margin-top: 0.25rem !important;
        margin-bottom: 0.25rem !important;
    }

    .my-sm-2 {
        margin-top: 0.5rem !important;
        margin-bottom: 0.5rem !important;
    }

    .my-sm-3 {
        margin-top: 1rem !important;
        margin-bottom: 1rem !important;
    }

    .my-sm-4 {
        margin-top: 1.5rem !important;
        margin-bottom: 1.5rem !important;
    }

    .my-sm-5 {
        margin-top: 3rem !important;
        margin-bottom: 3rem !important;
    }

    .my-sm-auto {
        margin-top: auto !important;
        margin-bottom: auto !important;
    }

    .mt-sm-0 {
        margin-top: 0 !important;
    }

    .mt-sm-1 {
        margin-top: 0.25rem !important;
    }

    .mt-sm-2 {
        margin-top: 0.5rem !important;
    }

    .mt-sm-3 {
        margin-top: 1rem !important;
    }

    .mt-sm-4 {
        margin-top: 1.5rem !important;
    }

    .mt-sm-5 {
        margin-top: 3rem !important;
    }

    .mt-sm-auto {
        margin-top: auto !important;
    }

    .me-sm-0 {
        margin-right: 0 !important;
    }

    .me-sm-1 {
        margin-right: 0.25rem !important;
    }

    .me-sm-2 {
        margin-right: 0.5rem !important;
    }

    .me-sm-3 {
        margin-right: 1rem !important;
    }

    .me-sm-4 {
        margin-right: 1.5rem !important;
    }

    .me-sm-5 {
        margin-right: 3rem !important;
    }

    .me-sm-auto {
        margin-right: auto !important;
    }

    .mb-sm-0 {
        margin-bottom: 0 !important;
    }

    .mb-sm-1 {
        margin-bottom: 0.25rem !important;
    }

    .mb-sm-2 {
        margin-bottom: 0.5rem !important;
    }

    .mb-sm-3 {
        margin-bottom: 1rem !important;
    }

    .mb-sm-4 {
        margin-bottom: 1.5rem !important;
    }

    .mb-sm-5 {
        margin-bottom: 3rem !important;
    }

    .mb-sm-auto {
        margin-bottom: auto !important;
    }

    .ms-sm-0 {
        margin-left: 0 !important;
    }

    .ms-sm-1 {
        margin-left: 0.25rem !important;
    }

    .ms-sm-2 {
        margin-left: 0.5rem !important;
    }

    .ms-sm-3 {
        margin-left: 1rem !important;
    }

    .ms-sm-4 {
        margin-left: 1.5rem !important;
    }

    .ms-sm-5 {
        margin-left: 3rem !important;
    }

    .ms-sm-auto {
        margin-left: auto !important;
    }

    .p-sm-0 {
        padding: 0 !important;
    }

    .p-sm-1 {
        padding: 0.25rem !important;
    }

    .p-sm-2 {
        padding: 0.5rem !important;
    }

    .p-sm-3 {
        padding: 1rem !important;
    }

    .p-sm-4 {
        padding: 1.5rem !important;
    }

    .p-sm-5 {
        padding: 3rem !important;
    }

    .px-sm-0 {
        padding-right: 0 !important;
        padding-left: 0 !important;
    }

    .px-sm-1 {
        padding-right: 0.25rem !important;
        padding-left: 0.25rem !important;
    }

    .px-sm-2 {
        padding-right: 0.5rem !important;
        padding-left: 0.5rem !important;
    }

    .px-sm-3 {
        padding-right: 1rem !important;
        padding-left: 1rem !important;
    }

    .px-sm-4 {
        padding-right: 1.5rem !important;
        padding-left: 1.5rem !important;
    }

    .px-sm-5 {
        padding-right: 3rem !important;
        padding-left: 3rem !important;
    }

    .py-sm-0 {
        padding-top: 0 !important;
        padding-bottom: 0 !important;
    }

    .py-sm-1 {
        padding-top: 0.25rem !important;
        padding-bottom: 0.25rem !important;
    }

    .py-sm-2 {
        padding-top: 0.5rem !important;
        padding-bottom: 0.5rem !important;
    }

    .py-sm-3 {
        padding-top: 1rem !important;
        padding-bottom: 1rem !important;
    }

    .py-sm-4 {
        padding-top: 1.5rem !important;
        padding-bottom: 1.5rem !important;
    }

    .py-sm-5 {
        padding-top: 3rem !important;
        padding-bottom: 3rem !important;
    }

    .pt-sm-0 {
        padding-top: 0 !important;
    }

    .pt-sm-1 {
        padding-top: 0.25rem !important;
    }

    .pt-sm-2 {
        padding-top: 0.5rem !important;
    }

    .pt-sm-3 {
        padding-top: 1rem !important;
    }

    .pt-sm-4 {
        padding-top: 1.5rem !important;
    }

    .pt-sm-5 {
        padding-top: 3rem !important;
    }

    .pe-sm-0 {
        padding-right: 0 !important;
    }

    .pe-sm-1 {
        padding-right: 0.25rem !important;
    }

    .pe-sm-2 {
        padding-right: 0.5rem !important;
    }

    .pe-sm-3 {
        padding-right: 1rem !important;
    }

    .pe-sm-4 {
        padding-right: 1.5rem !important;
    }

    .pe-sm-5 {
        padding-right: 3rem !important;
    }

    .pb-sm-0 {
        padding-bottom: 0 !important;
    }

    .pb-sm-1 {
        padding-bottom: 0.25rem !important;
    }

    .pb-sm-2 {
        padding-bottom: 0.5rem !important;
    }

    .pb-sm-3 {
        padding-bottom: 1rem !important;
    }

    .pb-sm-4 {
        padding-bottom: 1.5rem !important;
    }

    .pb-sm-5 {
        padding-bottom: 3rem !important;
    }

    .ps-sm-0 {
        padding-left: 0 !important;
    }

    .ps-sm-1 {
        padding-left: 0.25rem !important;
    }

    .ps-sm-2 {
        padding-left: 0.5rem !important;
    }

    .ps-sm-3 {
        padding-left: 1rem !important;
    }

    .ps-sm-4 {
        padding-left: 1.5rem !important;
    }

    .ps-sm-5 {
        padding-left: 3rem !important;
    }
}

@media (min-width: 768px) {
    .d-md-inline {
        display: inline !important;
    }

    .d-md-inline-block {
        display: inline-block !important;
    }

    .d-md-block {
        display: block !important;
    }

    .d-md-grid {
        display: grid !important;
    }

    .d-md-table {
        display: table !important;
    }

    .d-md-table-row {
        display: table-row !important;
    }

    .d-md-table-cell {
        display: table-cell !important;
    }

    .d-md-flex {
        display: flex !important;
    }

    .d-md-inline-flex {
        display: inline-flex !important;
    }

    .d-md-none {
        display: none !important;
    }

    .flex-md-fill {
        flex: 1 1 auto !important;
    }

    .flex-md-row {
        flex-direction: row !important;
    }

    .flex-md-column {
        flex-direction: column !important;
    }

    .flex-md-row-reverse {
        flex-direction: row-reverse !important;
    }

    .flex-md-column-reverse {
        flex-direction: column-reverse !important;
    }

    .flex-md-grow-0 {
        flex-grow: 0 !important;
    }

    .flex-md-grow-1 {
        flex-grow: 1 !important;
    }

    .flex-md-shrink-0 {
        flex-shrink: 0 !important;
    }

    .flex-md-shrink-1 {
        flex-shrink: 1 !important;
    }

    .flex-md-wrap {
        flex-wrap: wrap !important;
    }

    .flex-md-nowrap {
        flex-wrap: nowrap !important;
    }

    .flex-md-wrap-reverse {
        flex-wrap: wrap-reverse !important;
    }

    .justify-content-md-start {
        justify-content: flex-start !important;
    }

    .justify-content-md-end {
        justify-content: flex-end !important;
    }

    .justify-content-md-center {
        justify-content: center !important;
    }

    .justify-content-md-between {
        justify-content: space-between !important;
    }

    .justify-content-md-around {
        justify-content: space-around !important;
    }

    .justify-content-md-evenly {
        justify-content: space-evenly !important;
    }

    .align-items-md-start {
        align-items: flex-start !important;
    }

    .align-items-md-end {
        align-items: flex-end !important;
    }

    .align-items-md-center {
        align-items: center !important;
    }

    .align-items-md-baseline {
        align-items: baseline !important;
    }

    .align-items-md-stretch {
        align-items: stretch !important;
    }

    .align-content-md-start {
        align-content: flex-start !important;
    }

    .align-content-md-end {
        align-content: flex-end !important;
    }

    .align-content-md-center {
        align-content: center !important;
    }

    .align-content-md-between {
        align-content: space-between !important;
    }

    .align-content-md-around {
        align-content: space-around !important;
    }

    .align-content-md-stretch {
        align-content: stretch !important;
    }

    .align-self-md-auto {
        align-self: auto !important;
    }

    .align-self-md-start {
        align-self: flex-start !important;
    }

    .align-self-md-end {
        align-self: flex-end !important;
    }

    .align-self-md-center {
        align-self: center !important;
    }

    .align-self-md-baseline {
        align-self: baseline !important;
    }

    .align-self-md-stretch {
        align-self: stretch !important;
    }

    .order-md-first {
        order: -1 !important;
    }

    .order-md-0 {
        order: 0 !important;
    }

    .order-md-1 {
        order: 1 !important;
    }

    .order-md-2 {
        order: 2 !important;
    }

    .order-md-3 {
        order: 3 !important;
    }

    .order-md-4 {
        order: 4 !important;
    }

    .order-md-5 {
        order: 5 !important;
    }

    .order-md-last {
        order: 6 !important;
    }

    .m-md-0 {
        margin: 0 !important;
    }

    .m-md-1 {
        margin: 0.25rem !important;
    }

    .m-md-2 {
        margin: 0.5rem !important;
    }

    .m-md-3 {
        margin: 1rem !important;
    }

    .m-md-4 {
        margin: 1.5rem !important;
    }

    .m-md-5 {
        margin: 3rem !important;
    }

    .m-md-auto {
        margin: auto !important;
    }

    .mx-md-0 {
        margin-right: 0 !important;
        margin-left: 0 !important;
    }

    .mx-md-1 {
        margin-right: 0.25rem !important;
        margin-left: 0.25rem !important;
    }

    .mx-md-2 {
        margin-right: 0.5rem !important;
        margin-left: 0.5rem !important;
    }

    .mx-md-3 {
        margin-right: 1rem !important;
        margin-left: 1rem !important;
    }

    .mx-md-4 {
        margin-right: 1.5rem !important;
        margin-left: 1.5rem !important;
    }

    .mx-md-5 {
        margin-right: 3rem !important;
        margin-left: 3rem !important;
    }

    .mx-md-auto {
        margin-right: auto !important;
        margin-left: auto !important;
    }

    .my-md-0 {
        margin-top: 0 !important;
        margin-bottom: 0 !important;
    }

    .my-md-1 {
        margin-top: 0.25rem !important;
        margin-bottom: 0.25rem !important;
    }

    .my-md-2 {
        margin-top: 0.5rem !important;
        margin-bottom: 0.5rem !important;
    }

    .my-md-3 {
        margin-top: 1rem !important;
        margin-bottom: 1rem !important;
    }

    .my-md-4 {
        margin-top: 1.5rem !important;
        margin-bottom: 1.5rem !important;
    }

    .my-md-5 {
        margin-top: 3rem !important;
        margin-bottom: 3rem !important;
    }

    .my-md-auto {
        margin-top: auto !important;
        margin-bottom: auto !important;
    }

    .mt-md-0 {
        margin-top: 0 !important;
    }

    .mt-md-1 {
        margin-top: 0.25rem !important;
    }

    .mt-md-2 {
        margin-top: 0.5rem !important;
    }

    .mt-md-3 {
        margin-top: 1rem !important;
    }

    .mt-md-4 {
        margin-top: 1.5rem !important;
    }

    .mt-md-5 {
        margin-top: 3rem !important;
    }

    .mt-md-auto {
        margin-top: auto !important;
    }

    .me-md-0 {
        margin-right: 0 !important;
    }

    .me-md-1 {
        margin-right: 0.25rem !important;
    }

    .me-md-2 {
        margin-right: 0.5rem !important;
    }

    .me-md-3 {
        margin-right: 1rem !important;
    }

    .me-md-4 {
        margin-right: 1.5rem !important;
    }

    .me-md-5 {
        margin-right: 3rem !important;
    }

    .me-md-auto {
        margin-right: auto !important;
    }

    .mb-md-0 {
        margin-bottom: 0 !important;
    }

    .mb-md-1 {
        margin-bottom: 0.25rem !important;
    }

    .mb-md-2 {
        margin-bottom: 0.5rem !important;
    }

    .mb-md-3 {
        margin-bottom: 1rem !important;
    }

    .mb-md-4 {
        margin-bottom: 1.5rem !important;
    }

    .mb-md-5 {
        margin-bottom: 3rem !important;
    }

    .mb-md-auto {
        margin-bottom: auto !important;
    }

    .ms-md-0 {
        margin-left: 0 !important;
    }

    .ms-md-1 {
        margin-left: 0.25rem !important;
    }

    .ms-md-2 {
        margin-left: 0.5rem !important;
    }

    .ms-md-3 {
        margin-left: 1rem !important;
    }

    .ms-md-4 {
        margin-left: 1.5rem !important;
    }

    .ms-md-5 {
        margin-left: 3rem !important;
    }

    .ms-md-auto {
        margin-left: auto !important;
    }

    .p-md-0 {
        padding: 0 !important;
    }

    .p-md-1 {
        padding: 0.25rem !important;
    }

    .p-md-2 {
        padding: 0.5rem !important;
    }

    .p-md-3 {
        padding: 1rem !important;
    }

    .p-md-4 {
        padding: 1.5rem !important;
    }

    .p-md-5 {
        padding: 3rem !important;
    }

    .px-md-0 {
        padding-right: 0 !important;
        padding-left: 0 !important;
    }

    .px-md-1 {
        padding-right: 0.25rem !important;
        padding-left: 0.25rem !important;
    }

    .px-md-2 {
        padding-right: 0.5rem !important;
        padding-left: 0.5rem !important;
    }

    .px-md-3 {
        padding-right: 1rem !important;
        padding-left: 1rem !important;
    }

    .px-md-4 {
        padding-right: 1.5rem !important;
        padding-left: 1.5rem !important;
    }

    .px-md-5 {
        padding-right: 3rem !important;
        padding-left: 3rem !important;
    }

    .py-md-0 {
        padding-top: 0 !important;
        padding-bottom: 0 !important;
    }

    .py-md-1 {
        padding-top: 0.25rem !important;
        padding-bottom: 0.25rem !important;
    }

    .py-md-2 {
        padding-top: 0.5rem !important;
        padding-bottom: 0.5rem !important;
    }

    .py-md-3 {
        padding-top: 1rem !important;
        padding-bottom: 1rem !important;
    }

    .py-md-4 {
        padding-top: 1.5rem !important;
        padding-bottom: 1.5rem !important;
    }

    .py-md-5 {
        padding-top: 3rem !important;
        padding-bottom: 3rem !important;
    }

    .pt-md-0 {
        padding-top: 0 !important;
    }

    .pt-md-1 {
        padding-top: 0.25rem !important;
    }

    .pt-md-2 {
        padding-top: 0.5rem !important;
    }

    .pt-md-3 {
        padding-top: 1rem !important;
    }

    .pt-md-4 {
        padding-top: 1.5rem !important;
    }

    .pt-md-5 {
        padding-top: 3rem !important;
    }

    .pe-md-0 {
        padding-right: 0 !important;
    }

    .pe-md-1 {
        padding-right: 0.25rem !important;
    }

    .pe-md-2 {
        padding-right: 0.5rem !important;
    }

    .pe-md-3 {
        padding-right: 1rem !important;
    }

    .pe-md-4 {
        padding-right: 1.5rem !important;
    }

    .pe-md-5 {
        padding-right: 3rem !important;
    }

    .pb-md-0 {
        padding-bottom: 0 !important;
    }

    .pb-md-1 {
        padding-bottom: 0.25rem !important;
    }

    .pb-md-2 {
        padding-bottom: 0.5rem !important;
    }

    .pb-md-3 {
        padding-bottom: 1rem !important;
    }

    .pb-md-4 {
        padding-bottom: 1.5rem !important;
    }

    .pb-md-5 {
        padding-bottom: 3rem !important;
    }

    .ps-md-0 {
        padding-left: 0 !important;
    }

    .ps-md-1 {
        padding-left: 0.25rem !important;
    }

    .ps-md-2 {
        padding-left: 0.5rem !important;
    }

    .ps-md-3 {
        padding-left: 1rem !important;
    }

    .ps-md-4 {
        padding-left: 1.5rem !important;
    }

    .ps-md-5 {
        padding-left: 3rem !important;
    }
}

@media (min-width: 992px) {
    .d-lg-inline {
        display: inline !important;
    }

    .d-lg-inline-block {
        display: inline-block !important;
    }

    .d-lg-block {
        display: block !important;
    }

    .d-lg-grid {
        display: grid !important;
    }

    .d-lg-table {
        display: table !important;
    }

    .d-lg-table-row {
        display: table-row !important;
    }

    .d-lg-table-cell {
        display: table-cell !important;
    }

    .d-lg-flex {
        display: flex !important;
    }

    .d-lg-inline-flex {
        display: inline-flex !important;
    }

    .d-lg-none {
        display: none !important;
    }

    .flex-lg-fill {
        flex: 1 1 auto !important;
    }

    .flex-lg-row {
        flex-direction: row !important;
    }

    .flex-lg-column {
        flex-direction: column !important;
    }

    .flex-lg-row-reverse {
        flex-direction: row-reverse !important;
    }

    .flex-lg-column-reverse {
        flex-direction: column-reverse !important;
    }

    .flex-lg-grow-0 {
        flex-grow: 0 !important;
    }

    .flex-lg-grow-1 {
        flex-grow: 1 !important;
    }

    .flex-lg-shrink-0 {
        flex-shrink: 0 !important;
    }

    .flex-lg-shrink-1 {
        flex-shrink: 1 !important;
    }

    .flex-lg-wrap {
        flex-wrap: wrap !important;
    }

    .flex-lg-nowrap {
        flex-wrap: nowrap !important;
    }

    .flex-lg-wrap-reverse {
        flex-wrap: wrap-reverse !important;
    }

    .justify-content-lg-start {
        justify-content: flex-start !important;
    }

    .justify-content-lg-end {
        justify-content: flex-end !important;
    }

    .justify-content-lg-center {
        justify-content: center !important;
    }

    .justify-content-lg-between {
        justify-content: space-between !important;
    }

    .justify-content-lg-around {
        justify-content: space-around !important;
    }

    .justify-content-lg-evenly {
        justify-content: space-evenly !important;
    }

    .align-items-lg-start {
        align-items: flex-start !important;
    }

    .align-items-lg-end {
        align-items: flex-end !important;
    }

    .align-items-lg-center {
        align-items: center !important;
    }

    .align-items-lg-baseline {
        align-items: baseline !important;
    }

    .align-items-lg-stretch {
        align-items: stretch !important;
    }

    .align-content-lg-start {
        align-content: flex-start !important;
    }

    .align-content-lg-end {
        align-content: flex-end !important;
    }

    .align-content-lg-center {
        align-content: center !important;
    }

    .align-content-lg-between {
        align-content: space-between !important;
    }

    .align-content-lg-around {
        align-content: space-around !important;
    }

    .align-content-lg-stretch {
        align-content: stretch !important;
    }

    .align-self-lg-auto {
        align-self: auto !important;
    }

    .align-self-lg-start {
        align-self: flex-start !important;
    }

    .align-self-lg-end {
        align-self: flex-end !important;
    }

    .align-self-lg-center {
        align-self: center !important;
    }

    .align-self-lg-baseline {
        align-self: baseline !important;
    }

    .align-self-lg-stretch {
        align-self: stretch !important;
    }

    .order-lg-first {
        order: -1 !important;
    }

    .order-lg-0 {
        order: 0 !important;
    }

    .order-lg-1 {
        order: 1 !important;
    }

    .order-lg-2 {
        order: 2 !important;
    }

    .order-lg-3 {
        order: 3 !important;
    }

    .order-lg-4 {
        order: 4 !important;
    }

    .order-lg-5 {
        order: 5 !important;
    }

    .order-lg-last {
        order: 6 !important;
    }

    .m-lg-0 {
        margin: 0 !important;
    }

    .m-lg-1 {
        margin: 0.25rem !important;
    }

    .m-lg-2 {
        margin: 0.5rem !important;
    }

    .m-lg-3 {
        margin: 1rem !important;
    }

    .m-lg-4 {
        margin: 1.5rem !important;
    }

    .m-lg-5 {
        margin: 3rem !important;
    }

    .m-lg-auto {
        margin: auto !important;
    }

    .mx-lg-0 {
        margin-right: 0 !important;
        margin-left: 0 !important;
    }

    .mx-lg-1 {
        margin-right: 0.25rem !important;
        margin-left: 0.25rem !important;
    }

    .mx-lg-2 {
        margin-right: 0.5rem !important;
        margin-left: 0.5rem !important;
    }

    .mx-lg-3 {
        margin-right: 1rem !important;
        margin-left: 1rem !important;
    }

    .mx-lg-4 {
        margin-right: 1.5rem !important;
        margin-left: 1.5rem !important;
    }

    .mx-lg-5 {
        margin-right: 3rem !important;
        margin-left: 3rem !important;
    }

    .mx-lg-auto {
        margin-right: auto !important;
        margin-left: auto !important;
    }

    .my-lg-0 {
        margin-top: 0 !important;
        margin-bottom: 0 !important;
    }

    .my-lg-1 {
        margin-top: 0.25rem !important;
        margin-bottom: 0.25rem !important;
    }

    .my-lg-2 {
        margin-top: 0.5rem !important;
        margin-bottom: 0.5rem !important;
    }

    .my-lg-3 {
        margin-top: 1rem !important;
        margin-bottom: 1rem !important;
    }

    .my-lg-4 {
        margin-top: 1.5rem !important;
        margin-bottom: 1.5rem !important;
    }

    .my-lg-5 {
        margin-top: 3rem !important;
        margin-bottom: 3rem !important;
    }

    .my-lg-auto {
        margin-top: auto !important;
        margin-bottom: auto !important;
    }

    .mt-lg-0 {
        margin-top: 0 !important;
    }

    .mt-lg-1 {
        margin-top: 0.25rem !important;
    }

    .mt-lg-2 {
        margin-top: 0.5rem !important;
    }

    .mt-lg-3 {
        margin-top: 1rem !important;
    }

    .mt-lg-4 {
        margin-top: 1.5rem !important;
    }

    .mt-lg-5 {
        margin-top: 3rem !important;
    }

    .mt-lg-auto {
        margin-top: auto !important;
    }

    .me-lg-0 {
        margin-right: 0 !important;
    }

    .me-lg-1 {
        margin-right: 0.25rem !important;
    }

    .me-lg-2 {
        margin-right: 0.5rem !important;
    }

    .me-lg-3 {
        margin-right: 1rem !important;
    }

    .me-lg-4 {
        margin-right: 1.5rem !important;
    }

    .me-lg-5 {
        margin-right: 3rem !important;
    }

    .me-lg-auto {
        margin-right: auto !important;
    }

    .mb-lg-0 {
        margin-bottom: 0 !important;
    }

    .mb-lg-1 {
        margin-bottom: 0.25rem !important;
    }

    .mb-lg-2 {
        margin-bottom: 0.5rem !important;
    }

    .mb-lg-3 {
        margin-bottom: 1rem !important;
    }

    .mb-lg-4 {
        margin-bottom: 1.5rem !important;
    }

    .mb-lg-5 {
        margin-bottom: 3rem !important;
    }

    .mb-lg-auto {
        margin-bottom: auto !important;
    }

    .ms-lg-0 {
        margin-left: 0 !important;
    }

    .ms-lg-1 {
        margin-left: 0.25rem !important;
    }

    .ms-lg-2 {
        margin-left: 0.5rem !important;
    }

    .ms-lg-3 {
        margin-left: 1rem !important;
    }

    .ms-lg-4 {
        margin-left: 1.5rem !important;
    }

    .ms-lg-5 {
        margin-left: 3rem !important;
    }

    .ms-lg-auto {
        margin-left: auto !important;
    }

    .p-lg-0 {
        padding: 0 !important;
    }

    .p-lg-1 {
        padding: 0.25rem !important;
    }

    .p-lg-2 {
        padding: 0.5rem !important;
    }

    .p-lg-3 {
        padding: 1rem !important;
    }

    .p-lg-4 {
        padding: 1.5rem !important;
    }

    .p-lg-5 {
        padding: 3rem !important;
    }

    .px-lg-0 {
        padding-right: 0 !important;
        padding-left: 0 !important;
    }

    .px-lg-1 {
        padding-right: 0.25rem !important;
        padding-left: 0.25rem !important;
    }

    .px-lg-2 {
        padding-right: 0.5rem !important;
        padding-left: 0.5rem !important;
    }

    .px-lg-3 {
        padding-right: 1rem !important;
        padding-left: 1rem !important;
    }

    .px-lg-4 {
        padding-right: 1.5rem !important;
        padding-left: 1.5rem !important;
    }

    .px-lg-5 {
        padding-right: 3rem !important;
        padding-left: 3rem !important;
    }

    .py-lg-0 {
        padding-top: 0 !important;
        padding-bottom: 0 !important;
    }

    .py-lg-1 {
        padding-top: 0.25rem !important;
        padding-bottom: 0.25rem !important;
    }

    .py-lg-2 {
        padding-top: 0.5rem !important;
        padding-bottom: 0.5rem !important;
    }

    .py-lg-3 {
        padding-top: 1rem !important;
        padding-bottom: 1rem !important;
    }

    .py-lg-4 {
        padding-top: 1.5rem !important;
        padding-bottom: 1.5rem !important;
    }

    .py-lg-5 {
        padding-top: 3rem !important;
        padding-bottom: 3rem !important;
    }

    .pt-lg-0 {
        padding-top: 0 !important;
    }

    .pt-lg-1 {
        padding-top: 0.25rem !important;
    }

    .pt-lg-2 {
        padding-top: 0.5rem !important;
    }

    .pt-lg-3 {
        padding-top: 1rem !important;
    }

    .pt-lg-4 {
        padding-top: 1.5rem !important;
    }

    .pt-lg-5 {
        padding-top: 3rem !important;
    }

    .pe-lg-0 {
        padding-right: 0 !important;
    }

    .pe-lg-1 {
        padding-right: 0.25rem !important;
    }

    .pe-lg-2 {
        padding-right: 0.5rem !important;
    }

    .pe-lg-3 {
        padding-right: 1rem !important;
    }

    .pe-lg-4 {
        padding-right: 1.5rem !important;
    }

    .pe-lg-5 {
        padding-right: 3rem !important;
    }

    .pb-lg-0 {
        padding-bottom: 0 !important;
    }

    .pb-lg-1 {
        padding-bottom: 0.25rem !important;
    }

    .pb-lg-2 {
        padding-bottom: 0.5rem !important;
  
Download .txt
gitextract_p8znxfti/

├── .gitignore
├── .idea/
│   ├── artifacts/
│   │   └── webapp_hardware_bridge_jar.xml
│   ├── compiler.xml
│   ├── gradle.xml
│   ├── inspectionProfiles/
│   │   └── Project_Default.xml
│   ├── jarRepositories.xml
│   ├── misc.xml
│   ├── modules/
│   │   ├── webapp-hardware-bridge.iml
│   │   └── webapp-hardware-bridge.main.iml
│   ├── modules.xml
│   └── vcs.xml
├── ADVANCED.md
├── BUILD.md
├── CHANGELOG.md
├── CONFIGURATION.md
├── HTTP_API.md
├── LICENSE
├── README.md
├── TROUBLESHOOT.md
├── build.gradle
├── demo/
│   ├── printer-advanced.htm
│   ├── printer-annotation.htm
│   ├── printer-basic.htm
│   ├── serial-basic.html
│   ├── serial-weigh.htm
│   ├── websocket-printer.js
│   ├── websocket-serial.js
│   └── websocket-weigh.js
├── gradlew
├── gradlew.bat
├── install.nsi
├── settings.gradle
└── src/
    └── main/
        ├── java/
        │   ├── module-info.java
        │   └── tigerworkshop/
        │       └── webapphardwarebridge/
        │           ├── Constants.java
        │           ├── GUI.java
        │           ├── Server.java
        │           ├── dtos/
        │           │   ├── Config.java
        │           │   ├── NotificationDTO.java
        │           │   ├── PrintServiceDTO.java
        │           │   ├── SerialPortDTO.java
        │           │   └── VersionDTO.java
        │           ├── interfaces/
        │           │   ├── WebSocketServerInterface.java
        │           │   └── WebSocketServiceInterface.java
        │           ├── responses/
        │           │   ├── PrintDocument.java
        │           │   └── PrintResult.java
        │           ├── services/
        │           │   ├── ConfigService.java
        │           │   └── DocumentService.java
        │           ├── utils/
        │           │   ├── AnnotatedPrintable.java
        │           │   ├── CertificateGenerator.java
        │           │   ├── ImagePrintable.java
        │           │   └── ThreadUtil.java
        │           └── websocketservices/
        │               ├── PrinterWebSocketService.java
        │               └── SerialWebSocketService.java
        └── resources/
            ├── META-INF/
            │   └── MANIFEST.MF
            ├── log4j2.xml
            └── web/
                ├── css/
                │   ├── bootstrap-grid.css
                │   ├── bootstrap-grid.rtl.css
                │   ├── bootstrap-reboot.css
                │   ├── bootstrap-reboot.rtl.css
                │   ├── bootstrap-utilities.css
                │   ├── bootstrap-utilities.rtl.css
                │   ├── bootstrap.css
                │   └── bootstrap.rtl.css
                ├── index.html
                └── js/
                    ├── bootstrap.bundle.js
                    ├── bootstrap.esm.js
                    ├── bootstrap.js
                    └── petite-vue.js
Download .txt
SYMBOL INDEX (1326 symbols across 27 files)

FILE: demo/websocket-printer.js
  function WebSocketPrinter (line 1) | function WebSocketPrinter(options) {

FILE: demo/websocket-serial.js
  function WebSocketSerial (line 1) | function WebSocketSerial(options) {

FILE: demo/websocket-weigh.js
  function WebSocketWeigh (line 1) | function WebSocketWeigh(options) {

FILE: src/main/java/tigerworkshop/webapphardwarebridge/Constants.java
  class Constants (line 3) | public class Constants {

FILE: src/main/java/tigerworkshop/webapphardwarebridge/GUI.java
  class GUI (line 17) | @Log4j2
    method main (line 28) | public static void main(String[] args) throws Exception {
    method launch (line 33) | public void launch() throws Exception {
    method notify (line 117) | public void notify(String title, String message, TrayIcon.MessageType ...
    method restart (line 125) | public void restart() {
    method start (line 138) | @Override
    method stop (line 143) | @Override
    method messageToService (line 148) | @Override
    method messageToService (line 160) | @Override
    method onRegister (line 164) | @Override
    method onUnregister (line 169) | @Override
    method getChannel (line 173) | @Override

FILE: src/main/java/tigerworkshop/webapphardwarebridge/Server.java
  class Server (line 31) | @Log4j2
    method main (line 42) | public static void main(String[] args) {
    method start (line 50) | synchronized public void start() throws Exception {
    method stop (line 253) | synchronized public void stop() throws Exception {
    method messageToServer (line 267) | @Override
    method messageToServer (line 284) | @Override
    method messageToService (line 304) | @Override
    method messageToService (line 314) | @Override
    method registerService (line 324) | @Override
    method unregisterService (line 330) | @Override
    method getSocketsForChannel (line 339) | private ConcurrentLinkedQueue<WsContext> getSocketsForChannel(String c...
    method addSocketToChannel (line 343) | void addSocketToChannel(String channel, WsContext socket) {
    method removeSocketFromChannel (line 349) | private void removeSocketFromChannel(String channel, WsContext socket) {
    method getServicesForChannel (line 358) | private ConcurrentLinkedQueue<WebSocketServiceInterface> getServicesFo...
    method addServiceToChannel (line 367) | private void addServiceToChannel(String channel, WebSocketServiceInter...
    method removeServiceFromChannel (line 378) | private void removeServiceFromChannel(String channel, WebSocketService...

FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/Config.java
  class Config (line 13) | @Data
    method toJson (line 22) | public String toJson() throws JsonProcessingException {
    class GUI (line 26) | @Data
    class Notification (line 32) | @Data
    class Server (line 38) | @Data
      method getUri (line 47) | @JsonIgnore
    class Authentication (line 53) | @Data
    class TLS (line 60) | @Data
    class Downloader (line 70) | @Data
    class Printer (line 78) | @Data
    class Serial (line 87) | @Data
    class PrinterMapping (line 94) | @Data
    class SerialMapping (line 106) | @Data

FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/NotificationDTO.java
  class NotificationDTO (line 7) | @Getter

FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/PrintServiceDTO.java
  class PrintServiceDTO (line 6) | @NoArgsConstructor

FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/SerialPortDTO.java
  class SerialPortDTO (line 6) | @NoArgsConstructor

FILE: src/main/java/tigerworkshop/webapphardwarebridge/dtos/VersionDTO.java
  class VersionDTO (line 6) | @NoArgsConstructor

FILE: src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServerInterface.java
  type WebSocketServerInterface (line 4) | public interface WebSocketServerInterface {
    method messageToServer (line 5) | void messageToServer(String channel, String message);
    method messageToServer (line 7) | void messageToServer(String channel, byte[] message);
    method messageToService (line 9) | void messageToService(String channel, String message);
    method messageToService (line 11) | void messageToService(String channel, byte[] message);
    method registerService (line 13) | void registerService(WebSocketServiceInterface service);
    method unregisterService (line 15) | void unregisterService(WebSocketServiceInterface service);

FILE: src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServiceInterface.java
  type WebSocketServiceInterface (line 3) | public interface WebSocketServiceInterface {
    method start (line 4) | void start();
    method stop (line 6) | void stop();
    method messageToService (line 8) | void messageToService(String message);
    method messageToService (line 10) | void messageToService(byte[] message);
    method onRegister (line 12) | void onRegister(WebSocketServerInterface server);
    method onUnregister (line 14) | void onUnregister();
    method getChannel (line 16) | String getChannel();

FILE: src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintDocument.java
  class PrintDocument (line 11) | @ToString

FILE: src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintResult.java
  class PrintResult (line 7) | @ToString

FILE: src/main/java/tigerworkshop/webapphardwarebridge/services/ConfigService.java
  class ConfigService (line 13) | @Log4j2
    method ConfigService (line 27) | private ConfigService() {
    method loadFromJson (line 36) | public void loadFromJson(String json) throws JsonProcessingException {
    method loadFromFile (line 41) | public void loadFromFile(String filename) throws IOException {
    method save (line 46) | public void save() {
    method addPrintTypeToList (line 54) | public void addPrintTypeToList(String printType) {

FILE: src/main/java/tigerworkshop/webapphardwarebridge/services/DocumentService.java
  class DocumentService (line 24) | @Log4j2
    method prepareDocument (line 30) | public File prepareDocument(PrintDocument printDocument) throws Except...
    method deleteDocument (line 49) | public void deleteDocument(PrintDocument printDocument) throws IOExcep...
    method getOutputFile (line 53) | private File getOutputFile(PrintDocument printDocument) throws Malform...
    method download (line 64) | private void download(URL url, File outputFile) throws Exception {

FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/AnnotatedPrintable.java
  class AnnotatedPrintable (line 13) | @Log4j2
    method AnnotatedPrintable (line 20) | public AnnotatedPrintable(Printable printable) {
    method addAnnotation (line 24) | public void addAnnotation(AnnotatedPrintableAnnotation annotatedPrinta...
    method print (line 28) | @Override
    class AnnotatedPrintableAnnotation (line 78) | @Data

FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/CertificateGenerator.java
  class CertificateGenerator (line 27) | @Log4j2
    method generateSelfSignedCertificate (line 37) | public static void generateSelfSignedCertificate(String address, Strin...
    method isCertificateAndKeyExist (line 86) | public static Boolean isCertificateAndKeyExist(String certificatePath,...
    method saveCert (line 93) | private static void saveCert(X509Certificate cert, String certificateP...
    method saveKey (line 103) | private static void saveKey(PrivateKey key, String keyPath) {

FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/ImagePrintable.java
  class ImagePrintable (line 7) | public class ImagePrintable implements Printable {
    method ImagePrintable (line 10) | public ImagePrintable(Image image) {
    method print (line 14) | public int print(Graphics graphics, PageFormat pageFormat, int pageInd...

FILE: src/main/java/tigerworkshop/webapphardwarebridge/utils/ThreadUtil.java
  class ThreadUtil (line 3) | public class ThreadUtil {
    method silentSleep (line 4) | public static void silentSleep(long duration) {

FILE: src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/PrinterWebSocketService.java
  class PrinterWebSocketService (line 30) | @Log4j2
    method PrinterWebSocketService (line 38) | public PrinterWebSocketService() {
    method start (line 42) | @Override
    method stop (line 47) | @Override
    method messageToService (line 52) | @Override
    method messageToService (line 62) | @Override
    method onRegister (line 67) | @Override
    method onUnregister (line 72) | @Override
    method getChannel (line 77) | @Override
    method printDocument (line 85) | public void printDocument(PrintDocument printDocument) throws Exception {
    method isRaw (line 128) | private Boolean isRaw(PrintDocument printDocument) {
    method isImage (line 135) | private Boolean isImage(PrintDocument printDocument) {
    method isPDF (line 144) | private Boolean isPDF(PrintDocument printDocument) {
    method printRaw (line 153) | private void printRaw(PrintDocument printDocument, PrinterSearchResult...
    method printImage (line 170) | private void printImage(PrintDocument printDocument, PrinterSearchResu...
    method printPDF (line 208) | private void printPDF(PrintDocument printDocument, PrinterSearchResult...
    method getPageFormat (line 262) | private PageFormat getPageFormat(PrinterJob job, PrinterSearchResult p...
    method searchPrinterForType (line 287) | private PrinterSearchResult searchPrinterForType(String type) throws P...
    class PrinterSearchResult (line 325) | @Getter

FILE: src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/SerialWebSocketService.java
  class SerialWebSocketService (line 17) | @Log4j2
    method SerialWebSocketService (line 35) | public SerialWebSocketService(Config.SerialMapping newMapping) {
    method start (line 48) | @Override
    method stop (line 134) | @Override
    method messageToService (line 149) | @Override
    method messageToService (line 154) | @Override
    method onRegister (line 159) | @Override
    method onUnregister (line 164) | @Override
    method getChannel (line 169) | @Override

FILE: src/main/resources/web/js/bootstrap.bundle.js
  function makeEventUid (line 355) | function makeEventUid(element, uid) {
  function getElementEvents (line 359) | function getElementEvents(element) {
  function bootstrapHandler (line 366) | function bootstrapHandler(element, fn) {
  function bootstrapDelegationHandler (line 380) | function bootstrapDelegationHandler(element, selector, fn) {
  function findHandler (line 406) | function findHandler(events, callable, delegationSelector = null) {
  function normalizeParameters (line 410) | function normalizeParameters(originalTypeEvent, handler, delegationFunct...
  function addHandler (line 423) | function addHandler(element, originalTypeEvent, handler, delegationFunct...
  function removeHandler (line 462) | function removeHandler(element, events, typeEvent, handler, delegationSe...
  function removeNamespacedHandlers (line 473) | function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  function getTypeEvent (line 484) | function getTypeEvent(event) {
  method on (line 491) | on(element, event, handler, delegationFunction) {
  method one (line 495) | one(element, event, handler, delegationFunction) {
  method off (line 499) | off(element, originalTypeEvent, handler, delegationFunction) {
  method trigger (line 536) | trigger(element, event, args) {
  function hydrateObj (line 580) | function hydrateObj(obj, meta) {
  method set (line 611) | set(element, key, instance) {
  method get (line 628) | get(element, key) {
  method remove (line 636) | remove(element, key) {
  function normalizeData (line 657) | function normalizeData(value) {
  function normalizeDataKey (line 685) | function normalizeDataKey(key) {
  method setDataAttribute (line 690) | setDataAttribute(element, key, value) {
  method removeDataAttribute (line 694) | removeDataAttribute(element, key) {
  method getDataAttributes (line 698) | getDataAttributes(element) {
  method getDataAttribute (line 715) | getDataAttribute(element, key) {
  class Config (line 731) | class Config {
    method Default (line 733) | static get Default() {
    method DefaultType (line 737) | static get DefaultType() {
    method NAME (line 741) | static get NAME() {
    method _getConfig (line 745) | _getConfig(config) {
    method _configAfterMerge (line 754) | _configAfterMerge(config) {
    method _mergeConfigObj (line 758) | _mergeConfigObj(config, element) {
    method _typeCheckConfig (line 769) | _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
  class BaseComponent (line 799) | class BaseComponent extends Config {
    method constructor (line 800) | constructor(element, config) {
    method VERSION (line 813) | static get VERSION() {
    method DATA_KEY (line 817) | static get DATA_KEY() {
    method EVENT_KEY (line 821) | static get EVENT_KEY() {
    method getInstance (line 825) | static getInstance(element) {
    method getOrCreateInstance (line 829) | static getOrCreateInstance(element, config = {}) {
    method eventName (line 833) | static eventName(name) {
    method dispose (line 837) | dispose() {
    method _queueCallback (line 846) | _queueCallback(callback, element, isAnimated = true) {
    method _getConfig (line 850) | _getConfig(config) {
  class Alert (line 909) | class Alert extends BaseComponent {
    method NAME (line 911) | static get NAME() {
    method jQueryInterface (line 915) | static jQueryInterface(config) {
    method close (line 931) | close() {
    method _destroyElement (line 945) | _destroyElement() {
  class Button (line 988) | class Button extends BaseComponent {
    method NAME (line 990) | static get NAME() {
    method jQueryInterface (line 994) | static jQueryInterface(config) {
    method toggle (line 1004) | toggle() {
  method find (line 1039) | find(selector, element = document.documentElement) {
  method findOne (line 1043) | findOne(selector, element = document.documentElement) {
  method children (line 1047) | children(element, selector) {
  method parents (line 1051) | parents(element, selector) {
  method prev (line 1063) | prev(element, selector) {
  method next (line 1078) | next(element, selector) {
  method focusableChildren (line 1092) | focusableChildren(element) {
  class Swipe (line 1135) | class Swipe extends Config {
    method constructor (line 1136) | constructor(element, config) {
    method Default (line 1152) | static get Default() {
    method DefaultType (line 1156) | static get DefaultType() {
    method NAME (line 1160) | static get NAME() {
    method isSupported (line 1164) | static isSupported() {
    method dispose (line 1168) | dispose() {
    method _start (line 1172) | _start(event) {
    method _end (line 1183) | _end(event) {
    method _move (line 1193) | _move(event) {
    method _handleSwipe (line 1197) | _handleSwipe() {
    method _initEvents (line 1214) | _initEvents() {
    method _eventIsPointerPenTouch (line 1227) | _eventIsPointerPenTouch(event) {
  class Carousel (line 1303) | class Carousel extends BaseComponent {
    method constructor (line 1304) | constructor(element, config) {
    method Default (line 1321) | static get Default() {
    method DefaultType (line 1325) | static get DefaultType() {
    method NAME (line 1329) | static get NAME() {
    method jQueryInterface (line 1333) | static jQueryInterface(config) {
    method next (line 1352) | next() {
    method nextWhenVisible (line 1356) | nextWhenVisible() {
    method prev (line 1365) | prev() {
    method pause (line 1369) | pause() {
    method cycle (line 1377) | cycle() {
    method _maybeEnableCycle (line 1385) | _maybeEnableCycle() {
    method to (line 1398) | to(index) {
    method dispose (line 1421) | dispose() {
    method _configAfterMerge (line 1429) | _configAfterMerge(config) {
    method _addEventListeners (line 1434) | _addEventListeners() {
    method _addTouchEventListeners (line 1449) | _addTouchEventListeners() {
    method _keydown (line 1483) | _keydown(event) {
    method _getItemIndex (line 1497) | _getItemIndex(element) {
    method _setActiveIndicatorElement (line 1501) | _setActiveIndicatorElement(index) {
    method _updateInterval (line 1517) | _updateInterval() {
    method _slide (line 1528) | _slide(order, element = null) {
    method _isAnimated (line 1594) | _isAnimated() {
    method _getActive (line 1598) | _getActive() {
    method _getItems (line 1602) | _getItems() {
    method _clearInterval (line 1606) | _clearInterval() {
    method _directionToOrder (line 1613) | _directionToOrder(direction) {
    method _orderToDirection (line 1621) | _orderToDirection(order) {
  class Collapse (line 1722) | class Collapse extends BaseComponent {
    method constructor (line 1723) | constructor(element, config) {
    method Default (line 1750) | static get Default() {
    method DefaultType (line 1754) | static get DefaultType() {
    method NAME (line 1758) | static get NAME() {
    method jQueryInterface (line 1762) | static jQueryInterface(config) {
    method toggle (line 1782) | toggle() {
    method show (line 1790) | show() {
    method hide (line 1848) | hide() {
    method _isShown (line 1893) | _isShown(element = this._element) {
    method _configAfterMerge (line 1897) | _configAfterMerge(config) {
    method _getDimension (line 1904) | _getDimension() {
    method _initializeChildren (line 1908) | _initializeChildren() {
    method _getFirstLevelChildren (line 1924) | _getFirstLevelChildren(selector) {
    method _addAriaAndCollapsedClass (line 1930) | _addAriaAndCollapsedClass(triggerArray, isOpen) {
  function getNodeName (line 2001) | function getNodeName(element) {
  function getWindow (line 2005) | function getWindow(node) {
  function isElement (line 2018) | function isElement(node) {
  function isHTMLElement (line 2023) | function isHTMLElement(node) {
  function isShadowRoot (line 2028) | function isShadowRoot(node) {
  function applyStyles (line 2040) | function applyStyles(_ref) {
  function effect$2 (line 2067) | function effect$2(_ref2) {
  function getBasePlacement (line 2121) | function getBasePlacement(placement) {
  function getUAString (line 2129) | function getUAString() {
  function isLayoutViewport (line 2141) | function isLayoutViewport() {
  function getBoundingClientRect (line 2145) | function getBoundingClientRect(element, includeScale, isFixedStrategy) {
  function getLayoutRect (line 2185) | function getLayoutRect(element) {
  function contains (line 2208) | function contains(parent, child) {
  function getComputedStyle$1 (line 2231) | function getComputedStyle$1(element) {
  function isTableElement (line 2235) | function isTableElement(element) {
  function getDocumentElement (line 2239) | function getDocumentElement(element) {
  function getParentNode (line 2245) | function getParentNode(element) {
  function getTrueOffsetParent (line 2262) | function getTrueOffsetParent(element) {
  function getContainingBlock (line 2273) | function getContainingBlock(element) {
  function getOffsetParent (line 2309) | function getOffsetParent(element) {
  function getMainAxisFromPlacement (line 2324) | function getMainAxisFromPlacement(placement) {
  function within (line 2328) | function within(min$1, value, max$1) {
  function withinMaxClamp (line 2332) | function withinMaxClamp(min, value, max) {
  function getFreshSideObject (line 2337) | function getFreshSideObject() {
  function mergePaddingObject (line 2346) | function mergePaddingObject(paddingObject) {
  function expandToHashMap (line 2350) | function expandToHashMap(value, keys) {
  function arrow (line 2364) | function arrow(_ref) {
  function effect$1 (line 2401) | function effect$1(_ref2) {
  function getVariation (line 2439) | function getVariation(placement) {
  function roundOffsetsByDPR (line 2452) | function roundOffsetsByDPR(_ref) {
  function mapToStyles (line 2463) | function mapToStyles(_ref2) {
  function computeStyles (line 2555) | function computeStyles(_ref5) {
  function effect (line 2610) | function effect(_ref) {
  function getOppositePlacement (line 2662) | function getOppositePlacement(placement) {
  function getOppositeVariationPlacement (line 2673) | function getOppositeVariationPlacement(placement) {
  function getWindowScroll (line 2679) | function getWindowScroll(node) {
  function getWindowScrollBarX (line 2689) | function getWindowScrollBarX(element) {
  function getViewportRect (line 2700) | function getViewportRect(element, strategy) {
  function getDocumentRect (line 2730) | function getDocumentRect(element) {
  function isScrollParent (line 2753) | function isScrollParent(element) {
  function getScrollParent (line 2763) | function getScrollParent(node) {
  function listScrollParents (line 2783) | function listScrollParents(element, list) {
  function rectToClientRect (line 2799) | function rectToClientRect(rect) {
  function getInnerBoundingClientRect (line 2808) | function getInnerBoundingClientRect(element, strategy) {
  function getClientRectFromMixedType (line 2821) | function getClientRectFromMixedType(element, clippingParent, strategy) {
  function getClippingParents (line 2828) | function getClippingParents(element) {
  function getClippingRect (line 2845) | function getClippingRect(element, boundary, rootBoundary, strategy) {
  function computeOffsets (line 2864) | function computeOffsets(_ref) {
  function detectOverflow (line 2929) | function detectOverflow(state, options) {
  function computeAutoPlacement (line 2985) | function computeAutoPlacement(state, options) {
  function getExpandedFallbackPlacements (line 3025) | function getExpandedFallbackPlacements(placement) {
  function flip (line 3034) | function flip(_ref) {
  function getSideOffsets (line 3165) | function getSideOffsets(overflow, rect, preventedOffsets) {
  function isAnySideFullyClipped (line 3181) | function isAnySideFullyClipped(overflow) {
  function hide (line 3187) | function hide(_ref) {
  function distanceAndSkiddingToXY (line 3224) | function distanceAndSkiddingToXY(placement, rects, offset) {
  function offset (line 3245) | function offset(_ref2) {
  function popperOffsets (line 3276) | function popperOffsets(_ref) {
  function getAltAxis (line 3300) | function getAltAxis(axis) {
  function preventOverflow (line 3304) | function preventOverflow(_ref) {
  function getHTMLElementScroll (line 3435) | function getHTMLElementScroll(element) {
  function getNodeScroll (line 3442) | function getNodeScroll(node) {
  function isElementScaled (line 3450) | function isElementScaled(element) {
  function getCompositeRect (line 3459) | function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {
  function order (line 3500) | function order(modifiers) {
  function orderModifiers (line 3532) | function orderModifiers(modifiers) {
  function debounce (line 3543) | function debounce(fn) {
  function mergeByName (line 3559) | function mergeByName(modifiers) {
  function areValidElements (line 3580) | function areValidElements() {
  function popperGenerator (line 3590) | function popperGenerator(generatorOptions) {
  class Dropdown (line 3889) | class Dropdown extends BaseComponent {
    method constructor (line 3890) | constructor(element, config) {
    method Default (line 3901) | static get Default() {
    method DefaultType (line 3905) | static get DefaultType() {
    method NAME (line 3909) | static get NAME() {
    method jQueryInterface (line 3913) | static jQueryInterface(config) {
    method clearMenus (line 3929) | static clearMenus(event) {
    method dataApiKeydownHandler (line 3967) | static dataApiKeydownHandler(event) {
    method toggle (line 4004) | toggle() {
    method show (line 4008) | show() {
    method hide (line 4045) | hide() {
    method dispose (line 4057) | dispose() {
    method update (line 4065) | update() {
    method _completeHide (line 4073) | _completeHide(relatedTarget) {
    method _getConfig (line 4102) | _getConfig(config) {
    method _createPopper (line 4113) | _createPopper() {
    method _isShown (line 4133) | _isShown() {
    method _getPlacement (line 4137) | _getPlacement() {
    method _detectNavbar (line 4166) | _detectNavbar() {
    method _getOffset (line 4170) | _getOffset() {
    method _getPopperConfig (line 4186) | _getPopperConfig() {
    method _selectMenuItem (line 4217) | _selectMenuItem({
  class ScrollBarHelper (line 4272) | class ScrollBarHelper {
    method constructor (line 4273) | constructor() {
    method getWidth (line 4278) | getWidth() {
    method hide (line 4284) | hide() {
    method reset (line 4298) | reset() {
    method isOverflowing (line 4308) | isOverflowing() {
    method _disableOverFlow (line 4313) | _disableOverFlow() {
    method _setElementAttributes (line 4319) | _setElementAttributes(selector, styleProperty, callback) {
    method _saveInitialAttribute (line 4336) | _saveInitialAttribute(element, styleProperty) {
    method _resetElementAttributes (line 4344) | _resetElementAttributes(selector, styleProperty) {
    method _applyManipulationCallback (line 4360) | _applyManipulationCallback(selector, callBack) {
  class Backdrop (line 4408) | class Backdrop extends Config {
    method constructor (line 4409) | constructor(config) {
    method Default (line 4417) | static get Default() {
    method DefaultType (line 4421) | static get DefaultType() {
    method NAME (line 4425) | static get NAME() {
    method show (line 4430) | show(callback) {
    method hide (line 4451) | hide(callback) {
    method dispose (line 4465) | dispose() {
    method _getElement (line 4478) | _getElement() {
    method _configAfterMerge (line 4493) | _configAfterMerge(config) {
    method _append (line 4499) | _append() {
    method _emulateAnimation (line 4514) | _emulateAnimation(callback) {
  class FocusTrap (line 4552) | class FocusTrap extends Config {
    method constructor (line 4553) | constructor(config) {
    method Default (line 4561) | static get Default() {
    method DefaultType (line 4565) | static get DefaultType() {
    method NAME (line 4569) | static get NAME() {
    method activate (line 4574) | activate() {
    method deactivate (line 4590) | deactivate() {
    method _handleFocusin (line 4600) | _handleFocusin(event) {
    method _handleKeydown (line 4620) | _handleKeydown(event) {
  class Modal (line 4678) | class Modal extends BaseComponent {
    method constructor (line 4679) | constructor(element, config) {
    method Default (line 4692) | static get Default() {
    method DefaultType (line 4696) | static get DefaultType() {
    method NAME (line 4700) | static get NAME() {
    method jQueryInterface (line 4704) | static jQueryInterface(config, relatedTarget) {
    method toggle (line 4720) | toggle(relatedTarget) {
    method show (line 4724) | show(relatedTarget) {
    method hide (line 4749) | hide() {
    method dispose (line 4770) | dispose() {
    method handleUpdate (line 4782) | handleUpdate() {
    method _initializeBackDrop (line 4786) | _initializeBackDrop() {
    method _initializeFocusTrap (line 4794) | _initializeFocusTrap() {
    method _showElement (line 4800) | _showElement(relatedTarget) {
    method _addEventListeners (line 4839) | _addEventListeners() {
    method _hideModal (line 4878) | _hideModal() {
    method _isAnimated (line 4900) | _isAnimated() {
    method _triggerBackdropTransition (line 4904) | _triggerBackdropTransition() {
    method _adjustDialog (line 4940) | _adjustDialog() {
    method _resetAdjustments (line 4958) | _resetAdjustments() {
  class Offcanvas (line 5051) | class Offcanvas extends BaseComponent {
    method constructor (line 5052) | constructor(element, config) {
    method Default (line 5062) | static get Default() {
    method DefaultType (line 5066) | static get DefaultType() {
    method NAME (line 5070) | static get NAME() {
    method jQueryInterface (line 5074) | static jQueryInterface(config) {
    method toggle (line 5090) | toggle(relatedTarget) {
    method show (line 5094) | show(relatedTarget) {
    method hide (line 5138) | hide() {
    method dispose (line 5176) | dispose() {
    method _initializeBackDrop (line 5184) | _initializeBackDrop() {
    method _initializeFocusTrap (line 5205) | _initializeFocusTrap() {
    method _addEventListeners (line 5211) | _addEventListeners() {
  function sanitizeHtml (line 5351) | function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
  class TemplateFactory (line 5424) | class TemplateFactory extends Config {
    method constructor (line 5425) | constructor(config) {
    method Default (line 5431) | static get Default() {
    method DefaultType (line 5435) | static get DefaultType() {
    method NAME (line 5439) | static get NAME() {
    method getContent (line 5444) | getContent() {
    method hasContent (line 5448) | hasContent() {
    method changeContent (line 5452) | changeContent(content) {
    method toHtml (line 5462) | toHtml() {
    method _typeCheckConfig (line 5482) | _typeCheckConfig(config) {
    method _checkContent (line 5488) | _checkContent(arg) {
    method _setContent (line 5497) | _setContent(template, content, selector) {
    method _maybeSanitize (line 5525) | _maybeSanitize(arg) {
    method _resolvePossibleFunction (line 5529) | _resolvePossibleFunction(arg) {
    method _putElementInTemplate (line 5533) | _putElementInTemplate(element, templateElement) {
  class Tooltip (line 5627) | class Tooltip extends BaseComponent {
    method constructor (line 5628) | constructor(element, config) {
    method Default (line 5653) | static get Default() {
    method DefaultType (line 5657) | static get DefaultType() {
    method NAME (line 5661) | static get NAME() {
    method jQueryInterface (line 5665) | static jQueryInterface(config) {
    method enable (line 5681) | enable() {
    method disable (line 5685) | disable() {
    method toggleEnabled (line 5689) | toggleEnabled() {
    method toggle (line 5693) | toggle() {
    method dispose (line 5709) | dispose() {
    method show (line 5722) | show() {
    method hide (line 5781) | hide() {
    method update (line 5825) | update() {
    method _isWithContent (line 5831) | _isWithContent() {
    method _getTipElement (line 5835) | _getTipElement() {
    method _createTipElement (line 5843) | _createTipElement(content) {
    method setContent (line 5864) | setContent(content) {
    method _getTemplateFactory (line 5874) | _getTemplateFactory(content) {
    method _getContentForTemplate (line 5890) | _getContentForTemplate() {
    method _getTitle (line 5896) | _getTitle() {
    method _initializeOnDelegatedTarget (line 5900) | _initializeOnDelegatedTarget(event) {
    method _isAnimated (line 5904) | _isAnimated() {
    method _isShown (line 5908) | _isShown() {
    method _createPopper (line 5912) | _createPopper(tip) {
    method _getOffset (line 5918) | _getOffset() {
    method _resolvePossibleFunction (line 5934) | _resolvePossibleFunction(arg) {
    method _getPopperConfig (line 5938) | _getPopperConfig(attachment) {
    method _setListeners (line 5978) | _setListeners() {
    method _fixTitle (line 6017) | _fixTitle() {
    method _enter (line 6034) | _enter() {
    method _leave (line 6049) | _leave() {
    method _setTimeout (line 6063) | _setTimeout(handler, timeout) {
    method _isWithActiveTrigger (line 6068) | _isWithActiveTrigger() {
    method _getConfig (line 6072) | _getConfig(config) {
    method _configAfterMerge (line 6093) | _configAfterMerge(config) {
    method _getDelegateConfig (line 6114) | _getDelegateConfig() {
    method _disposePopper (line 6131) | _disposePopper() {
  class Popover (line 6183) | class Popover extends Tooltip {
    method Default (line 6185) | static get Default() {
    method DefaultType (line 6189) | static get DefaultType() {
    method NAME (line 6193) | static get NAME() {
    method jQueryInterface (line 6197) | static jQueryInterface(config) {
    method _isWithContent (line 6213) | _isWithContent() {
    method _getContentForTemplate (line 6217) | _getContentForTemplate() {
    method _getContent (line 6224) | _getContent() {
  class ScrollSpy (line 6286) | class ScrollSpy extends BaseComponent {
    method constructor (line 6287) | constructor(element, config) {
    method Default (line 6303) | static get Default() {
    method DefaultType (line 6307) | static get DefaultType() {
    method NAME (line 6311) | static get NAME() {
    method jQueryInterface (line 6315) | static jQueryInterface(config) {
    method refresh (line 6331) | refresh() {
    method dispose (line 6347) | dispose() {
    method _configAfterMerge (line 6353) | _configAfterMerge(config) {
    method _maybeEnableSmoothScroll (line 6366) | _maybeEnableSmoothScroll() {
    method _getNewObserver (line 6395) | _getNewObserver() {
    method _observerCallback (line 6404) | _observerCallback(entries) {
    method _initializeTargetsAndObservables (line 6445) | _initializeTargetsAndObservables() {
    method _process (line 6466) | _process(target) {
    method _activateParents (line 6483) | _activateParents(target) {
    method _clearActiveClass (line 6499) | _clearActiveClass(parent) {
  class Tab (line 6569) | class Tab extends BaseComponent {
    method constructor (line 6570) | constructor(element) {
    method NAME (line 6586) | static get NAME() {
    method jQueryInterface (line 6590) | static jQueryInterface(config) {
    method show (line 6606) | show() {
    method _activate (line 6633) | _activate(element, relatedElem) {
    method _deactivate (line 6662) | _deactivate(element, relatedElem) {
    method _keydown (line 6692) | _keydown(event) {
    method _getChildren (line 6711) | _getChildren() {
    method _getActiveElem (line 6716) | _getActiveElem() {
    method _setInitialAttributes (line 6720) | _setInitialAttributes(parent, children) {
    method _setInitialAttributesOnChild (line 6728) | _setInitialAttributesOnChild(child) {
    method _setInitialAttributesOnTargetPanel (line 6751) | _setInitialAttributesOnTargetPanel(child) {
    method _toggleDropDown (line 6765) | _toggleDropDown(element, open) {
    method _setAttributeIfNotExists (line 6785) | _setAttributeIfNotExists(element, attribute, value) {
    method _elemIsActive (line 6791) | _elemIsActive(elem) {
    method _getInnerElement (line 6795) | _getInnerElement(elem) {
    method _getOuterElement (line 6799) | _getOuterElement(elem) {
  class Toast (line 6877) | class Toast extends BaseComponent {
    method constructor (line 6878) | constructor(element, config) {
    method Default (line 6888) | static get Default() {
    method DefaultType (line 6892) | static get DefaultType() {
    method NAME (line 6896) | static get NAME() {
    method jQueryInterface (line 6900) | static jQueryInterface(config) {
    method show (line 6914) | show() {
    method hide (line 6945) | hide() {
    method dispose (line 6970) | dispose() {
    method isShown (line 6980) | isShown() {
    method _maybeScheduleHide (line 6984) | _maybeScheduleHide() {
    method _onInteraction (line 6998) | _onInteraction(event, isInteracting) {
    method _setListeners (line 7028) | _setListeners() {
    method _clearTimeout (line 7035) | _clearTimeout() {

FILE: src/main/resources/web/js/bootstrap.esm.js
  constant MAX_UID (line 14) | const MAX_UID = 1000000;
  constant MILLISECONDS_MULTIPLIER (line 15) | const MILLISECONDS_MULTIPLIER = 1000;
  constant TRANSITION_END (line 16) | const TRANSITION_END = 'transitionend';
  function makeEventUid (line 350) | function makeEventUid(element, uid) {
  function getElementEvents (line 354) | function getElementEvents(element) {
  function bootstrapHandler (line 361) | function bootstrapHandler(element, fn) {
  function bootstrapDelegationHandler (line 375) | function bootstrapDelegationHandler(element, selector, fn) {
  function findHandler (line 401) | function findHandler(events, callable, delegationSelector = null) {
  function normalizeParameters (line 405) | function normalizeParameters(originalTypeEvent, handler, delegationFunct...
  function addHandler (line 418) | function addHandler(element, originalTypeEvent, handler, delegationFunct...
  function removeHandler (line 457) | function removeHandler(element, events, typeEvent, handler, delegationSe...
  function removeNamespacedHandlers (line 468) | function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  function getTypeEvent (line 479) | function getTypeEvent(event) {
  method on (line 486) | on(element, event, handler, delegationFunction) {
  method one (line 490) | one(element, event, handler, delegationFunction) {
  method off (line 494) | off(element, originalTypeEvent, handler, delegationFunction) {
  method trigger (line 531) | trigger(element, event, args) {
  function hydrateObj (line 575) | function hydrateObj(obj, meta) {
  method set (line 606) | set(element, key, instance) {
  method get (line 623) | get(element, key) {
  method remove (line 631) | remove(element, key) {
  function normalizeData (line 652) | function normalizeData(value) {
  function normalizeDataKey (line 680) | function normalizeDataKey(key) {
  method setDataAttribute (line 685) | setDataAttribute(element, key, value) {
  method removeDataAttribute (line 689) | removeDataAttribute(element, key) {
  method getDataAttributes (line 693) | getDataAttributes(element) {
  method getDataAttribute (line 710) | getDataAttribute(element, key) {
  class Config (line 726) | class Config {
    method Default (line 728) | static get Default() {
    method DefaultType (line 732) | static get DefaultType() {
    method NAME (line 736) | static get NAME() {
    method _getConfig (line 740) | _getConfig(config) {
    method _configAfterMerge (line 749) | _configAfterMerge(config) {
    method _mergeConfigObj (line 753) | _mergeConfigObj(config, element) {
    method _typeCheckConfig (line 764) | _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
  constant VERSION (line 788) | const VERSION = '5.2.3';
  class BaseComponent (line 794) | class BaseComponent extends Config {
    method constructor (line 795) | constructor(element, config) {
    method VERSION (line 808) | static get VERSION() {
    method DATA_KEY (line 812) | static get DATA_KEY() {
    method EVENT_KEY (line 816) | static get EVENT_KEY() {
    method getInstance (line 820) | static getInstance(element) {
    method getOrCreateInstance (line 824) | static getOrCreateInstance(element, config = {}) {
    method eventName (line 828) | static eventName(name) {
    method dispose (line 832) | dispose() {
    method _queueCallback (line 841) | _queueCallback(callback, element, isAnimated = true) {
    method _getConfig (line 845) | _getConfig(config) {
  constant EVENT_CLOSE (line 895) | const EVENT_CLOSE = `close${EVENT_KEY$b}`;
  constant EVENT_CLOSED (line 896) | const EVENT_CLOSED = `closed${EVENT_KEY$b}`;
  class Alert (line 904) | class Alert extends BaseComponent {
    method NAME (line 906) | static get NAME() {
    method jQueryInterface (line 910) | static jQueryInterface(config) {
    method close (line 926) | close() {
    method _destroyElement (line 940) | _destroyElement() {
  class Button (line 983) | class Button extends BaseComponent {
    method NAME (line 985) | static get NAME() {
    method jQueryInterface (line 989) | static jQueryInterface(config) {
    method toggle (line 999) | toggle() {
  method find (line 1034) | find(selector, element = document.documentElement) {
  method findOne (line 1038) | findOne(selector, element = document.documentElement) {
  method children (line 1042) | children(element, selector) {
  method parents (line 1046) | parents(element, selector) {
  method prev (line 1058) | prev(element, selector) {
  method next (line 1073) | next(element, selector) {
  method focusableChildren (line 1087) | focusableChildren(element) {
  constant EVENT_TOUCHSTART (line 1106) | const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;
  constant EVENT_TOUCHMOVE (line 1107) | const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;
  constant EVENT_TOUCHEND (line 1108) | const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;
  constant EVENT_POINTERDOWN (line 1109) | const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;
  constant EVENT_POINTERUP (line 1110) | const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;
  constant POINTER_TYPE_TOUCH (line 1111) | const POINTER_TYPE_TOUCH = 'touch';
  constant POINTER_TYPE_PEN (line 1112) | const POINTER_TYPE_PEN = 'pen';
  constant CLASS_NAME_POINTER_EVENT (line 1113) | const CLASS_NAME_POINTER_EVENT = 'pointer-event';
  constant SWIPE_THRESHOLD (line 1114) | const SWIPE_THRESHOLD = 40;
  class Swipe (line 1130) | class Swipe extends Config {
    method constructor (line 1131) | constructor(element, config) {
    method Default (line 1147) | static get Default() {
    method DefaultType (line 1151) | static get DefaultType() {
    method NAME (line 1155) | static get NAME() {
    method isSupported (line 1159) | static isSupported() {
    method dispose (line 1163) | dispose() {
    method _start (line 1167) | _start(event) {
    method _end (line 1178) | _end(event) {
    method _move (line 1188) | _move(event) {
    method _handleSwipe (line 1192) | _handleSwipe() {
    method _initEvents (line 1209) | _initEvents() {
    method _eventIsPointerPenTouch (line 1222) | _eventIsPointerPenTouch(event) {
  constant TOUCHEVENT_COMPAT_WAIT (line 1244) | const TOUCHEVENT_COMPAT_WAIT = 500;
  constant ORDER_NEXT (line 1246) | const ORDER_NEXT = 'next';
  constant ORDER_PREV (line 1247) | const ORDER_PREV = 'prev';
  constant DIRECTION_LEFT (line 1248) | const DIRECTION_LEFT = 'left';
  constant DIRECTION_RIGHT (line 1249) | const DIRECTION_RIGHT = 'right';
  constant EVENT_SLIDE (line 1250) | const EVENT_SLIDE = `slide${EVENT_KEY$8}`;
  constant EVENT_SLID (line 1251) | const EVENT_SLID = `slid${EVENT_KEY$8}`;
  constant EVENT_DRAG_START (line 1255) | const EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;
  constant CLASS_NAME_CAROUSEL (line 1258) | const CLASS_NAME_CAROUSEL = 'carousel';
  constant CLASS_NAME_SLIDE (line 1260) | const CLASS_NAME_SLIDE = 'slide';
  constant CLASS_NAME_END (line 1261) | const CLASS_NAME_END = 'carousel-item-end';
  constant CLASS_NAME_START (line 1262) | const CLASS_NAME_START = 'carousel-item-start';
  constant CLASS_NAME_NEXT (line 1263) | const CLASS_NAME_NEXT = 'carousel-item-next';
  constant CLASS_NAME_PREV (line 1264) | const CLASS_NAME_PREV = 'carousel-item-prev';
  constant SELECTOR_ACTIVE (line 1265) | const SELECTOR_ACTIVE = '.active';
  constant SELECTOR_ITEM (line 1266) | const SELECTOR_ITEM = '.carousel-item';
  constant SELECTOR_ACTIVE_ITEM (line 1267) | const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;
  constant SELECTOR_ITEM_IMG (line 1268) | const SELECTOR_ITEM_IMG = '.carousel-item img';
  constant SELECTOR_INDICATORS (line 1269) | const SELECTOR_INDICATORS = '.carousel-indicators';
  constant SELECTOR_DATA_SLIDE (line 1270) | const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';
  constant SELECTOR_DATA_RIDE (line 1271) | const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]';
  constant KEY_TO_DIRECTION (line 1272) | const KEY_TO_DIRECTION = {
  class Carousel (line 1298) | class Carousel extends BaseComponent {
    method constructor (line 1299) | constructor(element, config) {
    method Default (line 1316) | static get Default() {
    method DefaultType (line 1320) | static get DefaultType() {
    method NAME (line 1324) | static get NAME() {
    method jQueryInterface (line 1328) | static jQueryInterface(config) {
    method next (line 1347) | next() {
    method nextWhenVisible (line 1351) | nextWhenVisible() {
    method prev (line 1360) | prev() {
    method pause (line 1364) | pause() {
    method cycle (line 1372) | cycle() {
    method _maybeEnableCycle (line 1380) | _maybeEnableCycle() {
    method to (line 1393) | to(index) {
    method dispose (line 1416) | dispose() {
    method _configAfterMerge (line 1424) | _configAfterMerge(config) {
    method _addEventListeners (line 1429) | _addEventListeners() {
    method _addTouchEventListeners (line 1444) | _addTouchEventListeners() {
    method _keydown (line 1478) | _keydown(event) {
    method _getItemIndex (line 1492) | _getItemIndex(element) {
    method _setActiveIndicatorElement (line 1496) | _setActiveIndicatorElement(index) {
    method _updateInterval (line 1512) | _updateInterval() {
    method _slide (line 1523) | _slide(order, element = null) {
    method _isAnimated (line 1589) | _isAnimated() {
    method _getActive (line 1593) | _getActive() {
    method _getItems (line 1597) | _getItems() {
    method _clearInterval (line 1601) | _clearInterval() {
    method _directionToOrder (line 1608) | _directionToOrder(direction) {
    method _orderToDirection (line 1616) | _orderToDirection(order) {
  constant CLASS_NAME_COLLAPSE (line 1695) | const CLASS_NAME_COLLAPSE = 'collapse';
  constant CLASS_NAME_COLLAPSING (line 1696) | const CLASS_NAME_COLLAPSING = 'collapsing';
  constant CLASS_NAME_COLLAPSED (line 1697) | const CLASS_NAME_COLLAPSED = 'collapsed';
  constant CLASS_NAME_DEEPER_CHILDREN (line 1698) | const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CL...
  constant CLASS_NAME_HORIZONTAL (line 1699) | const CLASS_NAME_HORIZONTAL = 'collapse-horizontal';
  constant WIDTH (line 1700) | const WIDTH = 'width';
  constant HEIGHT (line 1701) | const HEIGHT = 'height';
  constant SELECTOR_ACTIVES (line 1702) | const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';
  class Collapse (line 1717) | class Collapse extends BaseComponent {
    method constructor (line 1718) | constructor(element, config) {
    method Default (line 1745) | static get Default() {
    method DefaultType (line 1749) | static get DefaultType() {
    method NAME (line 1753) | static get NAME() {
    method jQueryInterface (line 1757) | static jQueryInterface(config) {
    method toggle (line 1777) | toggle() {
    method show (line 1785) | show() {
    method hide (line 1843) | hide() {
    method _isShown (line 1888) | _isShown(element = this._element) {
    method _configAfterMerge (line 1892) | _configAfterMerge(config) {
    method _getDimension (line 1899) | _getDimension() {
    method _initializeChildren (line 1903) | _initializeChildren() {
    method _getFirstLevelChildren (line 1919) | _getFirstLevelChildren(selector) {
    method _addAriaAndCollapsedClass (line 1925) | _addAriaAndCollapsedClass(triggerArray, isOpen) {
  constant RIGHT_MOUSE_BUTTON (line 1982) | const RIGHT_MOUSE_BUTTON = 2;
  constant EVENT_KEYDOWN_DATA_API (line 1989) | const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;
  constant EVENT_KEYUP_DATA_API (line 1990) | const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;
  constant CLASS_NAME_DROPUP (line 1992) | const CLASS_NAME_DROPUP = 'dropup';
  constant CLASS_NAME_DROPEND (line 1993) | const CLASS_NAME_DROPEND = 'dropend';
  constant CLASS_NAME_DROPSTART (line 1994) | const CLASS_NAME_DROPSTART = 'dropstart';
  constant CLASS_NAME_DROPUP_CENTER (line 1995) | const CLASS_NAME_DROPUP_CENTER = 'dropup-center';
  constant CLASS_NAME_DROPDOWN_CENTER (line 1996) | const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';
  constant SELECTOR_DATA_TOGGLE_SHOWN (line 1998) | const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NA...
  constant SELECTOR_MENU (line 1999) | const SELECTOR_MENU = '.dropdown-menu';
  constant SELECTOR_NAVBAR (line 2000) | const SELECTOR_NAVBAR = '.navbar';
  constant SELECTOR_NAVBAR_NAV (line 2001) | const SELECTOR_NAVBAR_NAV = '.navbar-nav';
  constant SELECTOR_VISIBLE_ITEMS (line 2002) | const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disab...
  constant PLACEMENT_TOP (line 2003) | const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';
  constant PLACEMENT_TOPEND (line 2004) | const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';
  constant PLACEMENT_BOTTOM (line 2005) | const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';
  constant PLACEMENT_BOTTOMEND (line 2006) | const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';
  constant PLACEMENT_RIGHT (line 2007) | const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';
  constant PLACEMENT_LEFT (line 2008) | const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';
  constant PLACEMENT_TOPCENTER (line 2009) | const PLACEMENT_TOPCENTER = 'top';
  constant PLACEMENT_BOTTOMCENTER (line 2010) | const PLACEMENT_BOTTOMCENTER = 'bottom';
  class Dropdown (line 2032) | class Dropdown extends BaseComponent {
    method constructor (line 2033) | constructor(element, config) {
    method Default (line 2044) | static get Default() {
    method DefaultType (line 2048) | static get DefaultType() {
    method NAME (line 2052) | static get NAME() {
    method jQueryInterface (line 2056) | static jQueryInterface(config) {
    method clearMenus (line 2072) | static clearMenus(event) {
    method dataApiKeydownHandler (line 2110) | static dataApiKeydownHandler(event) {
    method toggle (line 2147) | toggle() {
    method show (line 2151) | show() {
    method hide (line 2188) | hide() {
    method dispose (line 2200) | dispose() {
    method update (line 2208) | update() {
    method _completeHide (line 2216) | _completeHide(relatedTarget) {
    method _getConfig (line 2245) | _getConfig(config) {
    method _createPopper (line 2256) | _createPopper() {
    method _isShown (line 2276) | _isShown() {
    method _getPlacement (line 2280) | _getPlacement() {
    method _detectNavbar (line 2309) | _detectNavbar() {
    method _getOffset (line 2313) | _getOffset() {
    method _getPopperConfig (line 2329) | _getPopperConfig() {
    method _selectMenuItem (line 2360) | _selectMenuItem({
  constant SELECTOR_FIXED_CONTENT (line 2406) | const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .s...
  constant SELECTOR_STICKY_CONTENT (line 2407) | const SELECTOR_STICKY_CONTENT = '.sticky-top';
  constant PROPERTY_PADDING (line 2408) | const PROPERTY_PADDING = 'padding-right';
  constant PROPERTY_MARGIN (line 2409) | const PROPERTY_MARGIN = 'margin-right';
  class ScrollBarHelper (line 2415) | class ScrollBarHelper {
    method constructor (line 2416) | constructor() {
    method getWidth (line 2421) | getWidth() {
    method hide (line 2427) | hide() {
    method reset (line 2441) | reset() {
    method isOverflowing (line 2451) | isOverflowing() {
    method _disableOverFlow (line 2456) | _disableOverFlow() {
    method _setElementAttributes (line 2462) | _setElementAttributes(selector, styleProperty, callback) {
    method _saveInitialAttribute (line 2479) | _saveInitialAttribute(element, styleProperty) {
    method _resetElementAttributes (line 2487) | _resetElementAttributes(selector, styleProperty) {
    method _applyManipulationCallback (line 2503) | _applyManipulationCallback(selector, callBack) {
  constant EVENT_MOUSEDOWN (line 2529) | const EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;
  class Backdrop (line 2551) | class Backdrop extends Config {
    method constructor (line 2552) | constructor(config) {
    method Default (line 2560) | static get Default() {
    method DefaultType (line 2564) | static get DefaultType() {
    method NAME (line 2568) | static get NAME() {
    method show (line 2573) | show(callback) {
    method hide (line 2594) | hide(callback) {
    method dispose (line 2608) | dispose() {
    method _getElement (line 2621) | _getElement() {
    method _configAfterMerge (line 2636) | _configAfterMerge(config) {
    method _append (line 2642) | _append() {
    method _emulateAnimation (line 2657) | _emulateAnimation(callback) {
  constant EVENT_KEYDOWN_TAB (line 2677) | const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;
  constant TAB_KEY (line 2678) | const TAB_KEY = 'Tab';
  constant TAB_NAV_FORWARD (line 2679) | const TAB_NAV_FORWARD = 'forward';
  constant TAB_NAV_BACKWARD (line 2680) | const TAB_NAV_BACKWARD = 'backward';
  class FocusTrap (line 2695) | class FocusTrap extends Config {
    method constructor (line 2696) | constructor(config) {
    method Default (line 2704) | static get Default() {
    method DefaultType (line 2708) | static get DefaultType() {
    method NAME (line 2712) | static get NAME() {
    method activate (line 2717) | activate() {
    method deactivate (line 2733) | deactivate() {
    method _handleFocusin (line 2743) | _handleFocusin(event) {
    method _handleKeydown (line 2763) | _handleKeydown(event) {
  constant EVENT_CLICK_DISMISS (line 2794) | const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;
  constant EVENT_MOUSEDOWN_DISMISS (line 2795) | const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;
  constant CLASS_NAME_OPEN (line 2798) | const CLASS_NAME_OPEN = 'modal-open';
  constant CLASS_NAME_STATIC (line 2801) | const CLASS_NAME_STATIC = 'modal-static';
  constant SELECTOR_DIALOG (line 2803) | const SELECTOR_DIALOG = '.modal-dialog';
  constant SELECTOR_MODAL_BODY (line 2804) | const SELECTOR_MODAL_BODY = '.modal-body';
  class Modal (line 2821) | class Modal extends BaseComponent {
    method constructor (line 2822) | constructor(element, config) {
    method Default (line 2835) | static get Default() {
    method DefaultType (line 2839) | static get DefaultType() {
    method NAME (line 2843) | static get NAME() {
    method jQueryInterface (line 2847) | static jQueryInterface(config, relatedTarget) {
    method toggle (line 2863) | toggle(relatedTarget) {
    method show (line 2867) | show(relatedTarget) {
    method hide (line 2892) | hide() {
    method dispose (line 2913) | dispose() {
    method handleUpdate (line 2925) | handleUpdate() {
    method _initializeBackDrop (line 2929) | _initializeBackDrop() {
    method _initializeFocusTrap (line 2937) | _initializeFocusTrap() {
    method _showElement (line 2943) | _showElement(relatedTarget) {
    method _addEventListeners (line 2982) | _addEventListeners() {
    method _hideModal (line 3021) | _hideModal() {
    method _isAnimated (line 3043) | _isAnimated() {
    method _triggerBackdropTransition (line 3047) | _triggerBackdropTransition() {
    method _adjustDialog (line 3083) | _adjustDialog() {
    method _resetAdjustments (line 3101) | _resetAdjustments() {
  constant ESCAPE_KEY (line 3164) | const ESCAPE_KEY = 'Escape';
  constant CLASS_NAME_HIDING (line 3167) | const CLASS_NAME_HIDING = 'hiding';
  constant CLASS_NAME_BACKDROP (line 3168) | const CLASS_NAME_BACKDROP = 'offcanvas-backdrop';
  constant OPEN_SELECTOR (line 3169) | const OPEN_SELECTOR = '.offcanvas.show';
  constant EVENT_HIDE_PREVENTED (line 3173) | const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;
  constant EVENT_RESIZE (line 3175) | const EVENT_RESIZE = `resize${EVENT_KEY$3}`;
  constant EVENT_KEYDOWN_DISMISS (line 3177) | const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;
  class Offcanvas (line 3194) | class Offcanvas extends BaseComponent {
    method constructor (line 3195) | constructor(element, config) {
    method Default (line 3205) | static get Default() {
    method DefaultType (line 3209) | static get DefaultType() {
    method NAME (line 3213) | static get NAME() {
    method jQueryInterface (line 3217) | static jQueryInterface(config) {
    method toggle (line 3233) | toggle(relatedTarget) {
    method show (line 3237) | show(relatedTarget) {
    method hide (line 3281) | hide() {
    method dispose (line 3319) | dispose() {
    method _initializeBackDrop (line 3327) | _initializeBackDrop() {
    method _initializeFocusTrap (line 3348) | _initializeFocusTrap() {
    method _addEventListeners (line 3354) | _addEventListeners() {
  constant ARIA_ATTRIBUTE_PATTERN (line 3429) | const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i;
  constant SAFE_URL_PATTERN (line 3436) | const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:...
  constant DATA_URL_PATTERN (line 3443) | const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|w...
  function sanitizeHtml (line 3494) | function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
  class TemplateFactory (line 3567) | class TemplateFactory extends Config {
    method constructor (line 3568) | constructor(config) {
    method Default (line 3574) | static get Default() {
    method DefaultType (line 3578) | static get DefaultType() {
    method NAME (line 3582) | static get NAME() {
    method getContent (line 3587) | getContent() {
    method hasContent (line 3591) | hasContent() {
    method changeContent (line 3595) | changeContent(content) {
    method toHtml (line 3605) | toHtml() {
    method _typeCheckConfig (line 3625) | _typeCheckConfig(config) {
    method _checkContent (line 3631) | _checkContent(arg) {
    method _setContent (line 3640) | _setContent(template, content, selector) {
    method _maybeSanitize (line 3668) | _maybeSanitize(arg) {
    method _resolvePossibleFunction (line 3672) | _resolvePossibleFunction(arg) {
    method _putElementInTemplate (line 3676) | _putElementInTemplate(element, templateElement) {
  constant DISALLOWED_ATTRIBUTES (line 3699) | const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitiz...
  constant CLASS_NAME_MODAL (line 3701) | const CLASS_NAME_MODAL = 'modal';
  constant SELECTOR_TOOLTIP_INNER (line 3703) | const SELECTOR_TOOLTIP_INNER = '.tooltip-inner';
  constant SELECTOR_MODAL (line 3704) | const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;
  constant EVENT_MODAL_HIDE (line 3705) | const EVENT_MODAL_HIDE = 'hide.bs.modal';
  constant TRIGGER_HOVER (line 3706) | const TRIGGER_HOVER = 'hover';
  constant TRIGGER_FOCUS (line 3707) | const TRIGGER_FOCUS = 'focus';
  constant TRIGGER_CLICK (line 3708) | const TRIGGER_CLICK = 'click';
  constant TRIGGER_MANUAL (line 3709) | const TRIGGER_MANUAL = 'manual';
  constant EVENT_INSERTED (line 3714) | const EVENT_INSERTED = 'inserted';
  constant EVENT_MOUSEENTER (line 3718) | const EVENT_MOUSEENTER = 'mouseenter';
  constant EVENT_MOUSELEAVE (line 3719) | const EVENT_MOUSELEAVE = 'mouseleave';
  class Tooltip (line 3770) | class Tooltip extends BaseComponent {
    method constructor (line 3771) | constructor(element, config) {
    method Default (line 3796) | static get Default() {
    method DefaultType (line 3800) | static get DefaultType() {
    method NAME (line 3804) | static get NAME() {
    method jQueryInterface (line 3808) | static jQueryInterface(config) {
    method enable (line 3824) | enable() {
    method disable (line 3828) | disable() {
    method toggleEnabled (line 3832) | toggleEnabled() {
    method toggle (line 3836) | toggle() {
    method dispose (line 3852) | dispose() {
    method show (line 3865) | show() {
    method hide (line 3924) | hide() {
    method update (line 3968) | update() {
    method _isWithContent (line 3974) | _isWithContent() {
    method _getTipElement (line 3978) | _getTipElement() {
    method _createTipElement (line 3986) | _createTipElement(content) {
    method setContent (line 4007) | setContent(content) {
    method _getTemplateFactory (line 4017) | _getTemplateFactory(content) {
    method _getContentForTemplate (line 4033) | _getContentForTemplate() {
    method _getTitle (line 4039) | _getTitle() {
    method _initializeOnDelegatedTarget (line 4043) | _initializeOnDelegatedTarget(event) {
    method _isAnimated (line 4047) | _isAnimated() {
    method _isShown (line 4051) | _isShown() {
    method _createPopper (line 4055) | _createPopper(tip) {
    method _getOffset (line 4061) | _getOffset() {
    method _resolvePossibleFunction (line 4077) | _resolvePossibleFunction(arg) {
    method _getPopperConfig (line 4081) | _getPopperConfig(attachment) {
    method _setListeners (line 4121) | _setListeners() {
    method _fixTitle (line 4160) | _fixTitle() {
    method _enter (line 4177) | _enter() {
    method _leave (line 4192) | _leave() {
    method _setTimeout (line 4206) | _setTimeout(handler, timeout) {
    method _isWithActiveTrigger (line 4211) | _isWithActiveTrigger() {
    method _getConfig (line 4215) | _getConfig(config) {
    method _configAfterMerge (line 4236) | _configAfterMerge(config) {
    method _getDelegateConfig (line 4257) | _getDelegateConfig() {
    method _disposePopper (line 4274) | _disposePopper() {
  constant SELECTOR_TITLE (line 4307) | const SELECTOR_TITLE = '.popover-header';
  constant SELECTOR_CONTENT (line 4308) | const SELECTOR_CONTENT = '.popover-body';
  class Popover (line 4326) | class Popover extends Tooltip {
    method Default (line 4328) | static get Default() {
    method DefaultType (line 4332) | static get DefaultType() {
    method NAME (line 4336) | static get NAME() {
    method jQueryInterface (line 4340) | static jQueryInterface(config) {
    method _isWithContent (line 4356) | _isWithContent() {
    method _getContentForTemplate (line 4360) | _getContentForTemplate() {
    method _getContent (line 4367) | _getContent() {
  constant DATA_API_KEY (line 4393) | const DATA_API_KEY = '.data-api';
  constant EVENT_ACTIVATE (line 4394) | const EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;
  constant EVENT_CLICK (line 4395) | const EVENT_CLICK = `click${EVENT_KEY$2}`;
  constant CLASS_NAME_DROPDOWN_ITEM (line 4397) | const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';
  constant SELECTOR_DATA_SPY (line 4399) | const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]';
  constant SELECTOR_TARGET_LINKS (line 4400) | const SELECTOR_TARGET_LINKS = '[href]';
  constant SELECTOR_NAV_LIST_GROUP (line 4401) | const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';
  constant SELECTOR_NAV_LINKS (line 4402) | const SELECTOR_NAV_LINKS = '.nav-link';
  constant SELECTOR_NAV_ITEMS (line 4403) | const SELECTOR_NAV_ITEMS = '.nav-item';
  constant SELECTOR_LIST_ITEMS (line 4404) | const SELECTOR_LIST_ITEMS = '.list-group-item';
  constant SELECTOR_LINK_ITEMS (line 4405) | const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS...
  constant SELECTOR_DROPDOWN (line 4406) | const SELECTOR_DROPDOWN = '.dropdown';
  class ScrollSpy (line 4429) | class ScrollSpy extends BaseComponent {
    method constructor (line 4430) | constructor(element, config) {
    method Default (line 4446) | static get Default() {
    method DefaultType (line 4450) | static get DefaultType() {
    method NAME (line 4454) | static get NAME() {
    method jQueryInterface (line 4458) | static jQueryInterface(config) {
    method refresh (line 4474) | refresh() {
    method dispose (line 4490) | dispose() {
    method _configAfterMerge (line 4496) | _configAfterMerge(config) {
    method _maybeEnableSmoothScroll (line 4509) | _maybeEnableSmoothScroll() {
    method _getNewObserver (line 4538) | _getNewObserver() {
    method _observerCallback (line 4547) | _observerCallback(entries) {
    method _initializeTargetsAndObservables (line 4588) | _initializeTargetsAndObservables() {
    method _process (line 4609) | _process(target) {
    method _activateParents (line 4626) | _activateParents(target) {
    method _clearActiveClass (line 4642) | _clearActiveClass(parent) {
  constant EVENT_CLICK_DATA_API (line 4686) | const EVENT_CLICK_DATA_API = `click${EVENT_KEY$1}`;
  constant EVENT_KEYDOWN (line 4687) | const EVENT_KEYDOWN = `keydown${EVENT_KEY$1}`;
  constant EVENT_LOAD_DATA_API (line 4688) | const EVENT_LOAD_DATA_API = `load${EVENT_KEY$1}`;
  constant ARROW_LEFT_KEY (line 4689) | const ARROW_LEFT_KEY = 'ArrowLeft';
  constant ARROW_RIGHT_KEY (line 4690) | const ARROW_RIGHT_KEY = 'ArrowRight';
  constant ARROW_UP_KEY (line 4691) | const ARROW_UP_KEY = 'ArrowUp';
  constant ARROW_DOWN_KEY (line 4692) | const ARROW_DOWN_KEY = 'ArrowDown';
  constant CLASS_NAME_ACTIVE (line 4693) | const CLASS_NAME_ACTIVE = 'active';
  constant CLASS_DROPDOWN (line 4696) | const CLASS_DROPDOWN = 'dropdown';
  constant SELECTOR_DROPDOWN_TOGGLE (line 4697) | const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';
  constant SELECTOR_DROPDOWN_MENU (line 4698) | const SELECTOR_DROPDOWN_MENU = '.dropdown-menu';
  constant NOT_SELECTOR_DROPDOWN_TOGGLE (line 4699) | const NOT_SELECTOR_DROPDOWN_TOGGLE = ':not(.dropdown-toggle)';
  constant SELECTOR_TAB_PANEL (line 4700) | const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]';
  constant SELECTOR_OUTER (line 4701) | const SELECTOR_OUTER = '.nav-item, .list-group-item';
  constant SELECTOR_INNER (line 4702) | const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-...
  constant SELECTOR_DATA_TOGGLE (line 4703) | const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="p...
  constant SELECTOR_INNER_ELEM (line 4705) | const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`;
  constant SELECTOR_DATA_TOGGLE_ACTIVE (line 4706) | const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggl...
  class Tab (line 4712) | class Tab extends BaseComponent {
    method constructor (line 4713) | constructor(element) {
    method NAME (line 4729) | static get NAME() {
    method jQueryInterface (line 4733) | static jQueryInterface(config) {
    method show (line 4749) | show() {
    method _activate (line 4776) | _activate(element, relatedElem) {
    method _deactivate (line 4805) | _deactivate(element, relatedElem) {
    method _keydown (line 4835) | _keydown(event) {
    method _getChildren (line 4854) | _getChildren() {
    method _getActiveElem (line 4859) | _getActiveElem() {
    method _setInitialAttributes (line 4863) | _setInitialAttributes(parent, children) {
    method _setInitialAttributesOnChild (line 4871) | _setInitialAttributesOnChild(child) {
    method _setInitialAttributesOnTargetPanel (line 4894) | _setInitialAttributesOnTargetPanel(child) {
    method _toggleDropDown (line 4908) | _toggleDropDown(element, open) {
    method _setAttributeIfNotExists (line 4928) | _setAttributeIfNotExists(element, attribute, value) {
    method _elemIsActive (line 4934) | _elemIsActive(elem) {
    method _getInnerElement (line 4938) | _getInnerElement(elem) {
    method _getOuterElement (line 4942) | _getOuterElement(elem) {
  constant NAME (line 4989) | const NAME = 'toast';
  constant DATA_KEY (line 4990) | const DATA_KEY = 'bs.toast';
  constant EVENT_KEY (line 4991) | const EVENT_KEY = `.${DATA_KEY}`;
  constant EVENT_MOUSEOVER (line 4992) | const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`;
  constant EVENT_MOUSEOUT (line 4993) | const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`;
  constant EVENT_FOCUSIN (line 4994) | const EVENT_FOCUSIN = `focusin${EVENT_KEY}`;
  constant EVENT_FOCUSOUT (line 4995) | const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`;
  constant EVENT_HIDE (line 4996) | const EVENT_HIDE = `hide${EVENT_KEY}`;
  constant EVENT_HIDDEN (line 4997) | const EVENT_HIDDEN = `hidden${EVENT_KEY}`;
  constant EVENT_SHOW (line 4998) | const EVENT_SHOW = `show${EVENT_KEY}`;
  constant EVENT_SHOWN (line 4999) | const EVENT_SHOWN = `shown${EVENT_KEY}`;
  constant CLASS_NAME_FADE (line 5000) | const CLASS_NAME_FADE = 'fade';
  constant CLASS_NAME_HIDE (line 5001) | const CLASS_NAME_HIDE = 'hide';
  constant CLASS_NAME_SHOW (line 5003) | const CLASS_NAME_SHOW = 'show';
  constant CLASS_NAME_SHOWING (line 5004) | const CLASS_NAME_SHOWING = 'showing';
  class Toast (line 5020) | class Toast extends BaseComponent {
    method constructor (line 5021) | constructor(element, config) {
    method Default (line 5031) | static get Default() {
    method DefaultType (line 5035) | static get DefaultType() {
    method NAME (line 5039) | static get NAME() {
    method jQueryInterface (line 5043) | static jQueryInterface(config) {
    method show (line 5057) | show() {
    method hide (line 5088) | hide() {
    method dispose (line 5113) | dispose() {
    method isShown (line 5123) | isShown() {
    method _maybeScheduleHide (line 5127) | _maybeScheduleHide() {
    method _onInteraction (line 5141) | _onInteraction(event, isInteracting) {
    method _setListeners (line 5171) | _setListeners() {
    method _clearTimeout (line 5178) | _clearTimeout() {

FILE: src/main/resources/web/js/bootstrap.js
  function _interopNamespace (line 13) | function _interopNamespace(e) {
  function makeEventUid (line 375) | function makeEventUid(element, uid) {
  function getElementEvents (line 379) | function getElementEvents(element) {
  function bootstrapHandler (line 386) | function bootstrapHandler(element, fn) {
  function bootstrapDelegationHandler (line 400) | function bootstrapDelegationHandler(element, selector, fn) {
  function findHandler (line 426) | function findHandler(events, callable, delegationSelector = null) {
  function normalizeParameters (line 430) | function normalizeParameters(originalTypeEvent, handler, delegationFunct...
  function addHandler (line 443) | function addHandler(element, originalTypeEvent, handler, delegationFunct...
  function removeHandler (line 482) | function removeHandler(element, events, typeEvent, handler, delegationSe...
  function removeNamespacedHandlers (line 493) | function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  function getTypeEvent (line 504) | function getTypeEvent(event) {
  method on (line 511) | on(element, event, handler, delegationFunction) {
  method one (line 515) | one(element, event, handler, delegationFunction) {
  method off (line 519) | off(element, originalTypeEvent, handler, delegationFunction) {
  method trigger (line 556) | trigger(element, event, args) {
  function hydrateObj (line 600) | function hydrateObj(obj, meta) {
  method set (line 631) | set(element, key, instance) {
  method get (line 648) | get(element, key) {
  method remove (line 656) | remove(element, key) {
  function normalizeData (line 677) | function normalizeData(value) {
  function normalizeDataKey (line 705) | function normalizeDataKey(key) {
  method setDataAttribute (line 710) | setDataAttribute(element, key, value) {
  method removeDataAttribute (line 714) | removeDataAttribute(element, key) {
  method getDataAttributes (line 718) | getDataAttributes(element) {
  method getDataAttribute (line 735) | getDataAttribute(element, key) {
  class Config (line 751) | class Config {
    method Default (line 753) | static get Default() {
    method DefaultType (line 757) | static get DefaultType() {
    method NAME (line 761) | static get NAME() {
    method _getConfig (line 765) | _getConfig(config) {
    method _configAfterMerge (line 774) | _configAfterMerge(config) {
    method _mergeConfigObj (line 778) | _mergeConfigObj(config, element) {
    method _typeCheckConfig (line 789) | _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
  class BaseComponent (line 819) | class BaseComponent extends Config {
    method constructor (line 820) | constructor(element, config) {
    method VERSION (line 833) | static get VERSION() {
    method DATA_KEY (line 837) | static get DATA_KEY() {
    method EVENT_KEY (line 841) | static get EVENT_KEY() {
    method getInstance (line 845) | static getInstance(element) {
    method getOrCreateInstance (line 849) | static getOrCreateInstance(element, config = {}) {
    method eventName (line 853) | static eventName(name) {
    method dispose (line 857) | dispose() {
    method _queueCallback (line 866) | _queueCallback(callback, element, isAnimated = true) {
    method _getConfig (line 870) | _getConfig(config) {
  class Alert (line 929) | class Alert extends BaseComponent {
    method NAME (line 931) | static get NAME() {
    method jQueryInterface (line 935) | static jQueryInterface(config) {
    method close (line 951) | close() {
    method _destroyElement (line 965) | _destroyElement() {
  class Button (line 1008) | class Button extends BaseComponent {
    method NAME (line 1010) | static get NAME() {
    method jQueryInterface (line 1014) | static jQueryInterface(config) {
    method toggle (line 1024) | toggle() {
  method find (line 1059) | find(selector, element = document.documentElement) {
  method findOne (line 1063) | findOne(selector, element = document.documentElement) {
  method children (line 1067) | children(element, selector) {
  method parents (line 1071) | parents(element, selector) {
  method prev (line 1083) | prev(element, selector) {
  method next (line 1098) | next(element, selector) {
  method focusableChildren (line 1112) | focusableChildren(element) {
  class Swipe (line 1155) | class Swipe extends Config {
    method constructor (line 1156) | constructor(element, config) {
    method Default (line 1172) | static get Default() {
    method DefaultType (line 1176) | static get DefaultType() {
    method NAME (line 1180) | static get NAME() {
    method isSupported (line 1184) | static isSupported() {
    method dispose (line 1188) | dispose() {
    method _start (line 1192) | _start(event) {
    method _end (line 1203) | _end(event) {
    method _move (line 1213) | _move(event) {
    method _handleSwipe (line 1217) | _handleSwipe() {
    method _initEvents (line 1234) | _initEvents() {
    method _eventIsPointerPenTouch (line 1247) | _eventIsPointerPenTouch(event) {
  class Carousel (line 1323) | class Carousel extends BaseComponent {
    method constructor (line 1324) | constructor(element, config) {
    method Default (line 1341) | static get Default() {
    method DefaultType (line 1345) | static get DefaultType() {
    method NAME (line 1349) | static get NAME() {
    method jQueryInterface (line 1353) | static jQueryInterface(config) {
    method next (line 1372) | next() {
    method nextWhenVisible (line 1376) | nextWhenVisible() {
    method prev (line 1385) | prev() {
    method pause (line 1389) | pause() {
    method cycle (line 1397) | cycle() {
    method _maybeEnableCycle (line 1405) | _maybeEnableCycle() {
    method to (line 1418) | to(index) {
    method dispose (line 1441) | dispose() {
    method _configAfterMerge (line 1449) | _configAfterMerge(config) {
    method _addEventListeners (line 1454) | _addEventListeners() {
    method _addTouchEventListeners (line 1469) | _addTouchEventListeners() {
    method _keydown (line 1503) | _keydown(event) {
    method _getItemIndex (line 1517) | _getItemIndex(element) {
    method _setActiveIndicatorElement (line 1521) | _setActiveIndicatorElement(index) {
    method _updateInterval (line 1537) | _updateInterval() {
    method _slide (line 1548) | _slide(order, element = null) {
    method _isAnimated (line 1614) | _isAnimated() {
    method _getActive (line 1618) | _getActive() {
    method _getItems (line 1622) | _getItems() {
    method _clearInterval (line 1626) | _clearInterval() {
    method _directionToOrder (line 1633) | _directionToOrder(direction) {
    method _orderToDirection (line 1641) | _orderToDirection(order) {
  class Collapse (line 1742) | class Collapse extends BaseComponent {
    method constructor (line 1743) | constructor(element, config) {
    method Default (line 1770) | static get Default() {
    method DefaultType (line 1774) | static get DefaultType() {
    method NAME (line 1778) | static get NAME() {
    method jQueryInterface (line 1782) | static jQueryInterface(config) {
    method toggle (line 1802) | toggle() {
    method show (line 1810) | show() {
    method hide (line 1868) | hide() {
    method _isShown (line 1913) | _isShown(element = this._element) {
    method _configAfterMerge (line 1917) | _configAfterMerge(config) {
    method _getDimension (line 1924) | _getDimension() {
    method _initializeChildren (line 1928) | _initializeChildren() {
    method _getFirstLevelChildren (line 1944) | _getFirstLevelChildren(selector) {
    method _addAriaAndCollapsedClass (line 1950) | _addAriaAndCollapsedClass(triggerArray, isOpen) {
  class Dropdown (line 2057) | class Dropdown extends BaseComponent {
    method constructor (line 2058) | constructor(element, config) {
    method Default (line 2069) | static get Default() {
    method DefaultType (line 2073) | static get DefaultType() {
    method NAME (line 2077) | static get NAME() {
    method jQueryInterface (line 2081) | static jQueryInterface(config) {
    method clearMenus (line 2097) | static clearMenus(event) {
    method dataApiKeydownHandler (line 2135) | static dataApiKeydownHandler(event) {
    method toggle (line 2172) | toggle() {
    method show (line 2176) | show() {
    method hide (line 2213) | hide() {
    method dispose (line 2225) | dispose() {
    method update (line 2233) | update() {
    method _completeHide (line 2241) | _completeHide(relatedTarget) {
    method _getConfig (line 2270) | _getConfig(config) {
    method _createPopper (line 2281) | _createPopper() {
    method _isShown (line 2301) | _isShown() {
    method _getPlacement (line 2305) | _getPlacement() {
    method _detectNavbar (line 2334) | _detectNavbar() {
    method _getOffset (line 2338) | _getOffset() {
    method _getPopperConfig (line 2354) | _getPopperConfig() {
    method _selectMenuItem (line 2385) | _selectMenuItem({
  class ScrollBarHelper (line 2440) | class ScrollBarHelper {
    method constructor (line 2441) | constructor() {
    method getWidth (line 2446) | getWidth() {
    method hide (line 2452) | hide() {
    method reset (line 2466) | reset() {
    method isOverflowing (line 2476) | isOverflowing() {
    method _disableOverFlow (line 2481) | _disableOverFlow() {
    method _setElementAttributes (line 2487) | _setElementAttributes(selector, styleProperty, callback) {
    method _saveInitialAttribute (line 2504) | _saveInitialAttribute(element, styleProperty) {
    method _resetElementAttributes (line 2512) | _resetElementAttributes(selector, styleProperty) {
    method _applyManipulationCallback (line 2528) | _applyManipulationCallback(selector, callBack) {
  class Backdrop (line 2576) | class Backdrop extends Config {
    method constructor (line 2577) | constructor(config) {
    method Default (line 2585) | static get Default() {
    method DefaultType (line 2589) | static get DefaultType() {
    method NAME (line 2593) | static get NAME() {
    method show (line 2598) | show(callback) {
    method hide (line 2619) | hide(callback) {
    method dispose (line 2633) | dispose() {
    method _getElement (line 2646) | _getElement() {
    method _configAfterMerge (line 2661) | _configAfterMerge(config) {
    method _append (line 2667) | _append() {
    method _emulateAnimation (line 2682) | _emulateAnimation(callback) {
  class FocusTrap (line 2720) | class FocusTrap extends Config {
    method constructor (line 2721) | constructor(config) {
    method Default (line 2729) | static get Default() {
    method DefaultType (line 2733) | static get DefaultType() {
    method NAME (line 2737) | static get NAME() {
    method activate (line 2742) | activate() {
    method deactivate (line 2758) | deactivate() {
    method _handleFocusin (line 2768) | _handleFocusin(event) {
    method _handleKeydown (line 2788) | _handleKeydown(event) {
  class Modal (line 2846) | class Modal extends BaseComponent {
    method constructor (line 2847) | constructor(element, config) {
    method Default (line 2860) | static get Default() {
    method DefaultType (line 2864) | static get DefaultType() {
    method NAME (line 2868) | static get NAME() {
    method jQueryInterface (line 2872) | static jQueryInterface(config, relatedTarget) {
    method toggle (line 2888) | toggle(relatedTarget) {
    method show (line 2892) | show(relatedTarget) {
    method hide (line 2917) | hide() {
    method dispose (line 2938) | dispose() {
    method handleUpdate (line 2950) | handleUpdate() {
    method _initializeBackDrop (line 2954) | _initializeBackDrop() {
    method _initializeFocusTrap (line 2962) | _initializeFocusTrap() {
    method _showElement (line 2968) | _showElement(relatedTarget) {
    method _addEventListeners (line 3007) | _addEventListeners() {
    method _hideModal (line 3046) | _hideModal() {
    method _isAnimated (line 3068) | _isAnimated() {
    method _triggerBackdropTransition (line 3072) | _triggerBackdropTransition() {
    method _adjustDialog (line 3108) | _adjustDialog() {
    method _resetAdjustments (line 3126) | _resetAdjustments() {
  class Offcanvas (line 3219) | class Offcanvas extends BaseComponent {
    method constructor (line 3220) | constructor(element, config) {
    method Default (line 3230) | static get Default() {
    method DefaultType (line 3234) | static get DefaultType() {
    method NAME (line 3238) | static get NAME() {
    method jQueryInterface (line 3242) | static jQueryInterface(config) {
    method toggle (line 3258) | toggle(relatedTarget) {
    method show (line 3262) | show(relatedTarget) {
    method hide (line 3306) | hide() {
    method dispose (line 3344) | dispose() {
    method _initializeBackDrop (line 3352) | _initializeBackDrop() {
    method _initializeFocusTrap (line 3373) | _initializeFocusTrap() {
    method _addEventListeners (line 3379) | _addEventListeners() {
  function sanitizeHtml (line 3519) | function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
  class TemplateFactory (line 3592) | class TemplateFactory extends Config {
    method constructor (line 3593) | constructor(config) {
    method Default (line 3599) | static get Default() {
    method DefaultType (line 3603) | static get DefaultType() {
    method NAME (line 3607) | static get NAME() {
    method getContent (line 3612) | getContent() {
    method hasContent (line 3616) | hasContent() {
    method changeContent (line 3620) | changeContent(content) {
    method toHtml (line 3630) | toHtml() {
    method _typeCheckConfig (line 3650) | _typeCheckConfig(config) {
    method _checkContent (line 3656) | _checkContent(arg) {
    method _setContent (line 3665) | _setContent(template, content, selector) {
    method _maybeSanitize (line 3693) | _maybeSanitize(arg) {
    method _resolvePossibleFunction (line 3697) | _resolvePossibleFunction(arg) {
    method _putElementInTemplate (line 3701) | _putElementInTemplate(element, templateElement) {
  class Tooltip (line 3795) | class Tooltip extends BaseComponent {
    method constructor (line 3796) | constructor(element, config) {
    method Default (line 3821) | static get Default() {
    method DefaultType (line 3825) | static get DefaultType() {
    method NAME (line 3829) | static get NAME() {
    method jQueryInterface (line 3833) | static jQueryInterface(config) {
    method enable (line 3849) | enable() {
    method disable (line 3853) | disable() {
    method toggleEnabled (line 3857) | toggleEnabled() {
    method toggle (line 3861) | toggle() {
    method dispose (line 3877) | dispose() {
    method show (line 3890) | show() {
    method hide (line 3949) | hide() {
    method update (line 3993) | update() {
    method _isWithContent (line 3999) | _isWithContent() {
    method _getTipElement (line 4003) | _getTipElement() {
    method _createTipElement (line 4011) | _createTipElement(content) {
    method setContent (line 4032) | setContent(content) {
    method _getTemplateFactory (line 4042) | _getTemplateFactory(content) {
    method _getContentForTemplate (line 4058) | _getContentForTemplate() {
    method _getTitle (line 4064) | _getTitle() {
    method _initializeOnDelegatedTarget (line 4068) | _initializeOnDelegatedTarget(event) {
    method _isAnimated (line 4072) | _isAnimated() {
    method _isShown (line 4076) | _isShown() {
    method _createPopper (line 4080) | _createPopper(tip) {
    method _getOffset (line 4086) | _getOffset() {
    method _resolvePossibleFunction (line 4102) | _resolvePossibleFunction(arg) {
    method _getPopperConfig (line 4106) | _getPopperConfig(attachment) {
    method _setListeners (line 4146) | _setListeners() {
    method _fixTitle (line 4185) | _fixTitle() {
    method _enter (line 4202) | _enter() {
    method _leave (line 4217) | _leave() {
    method _setTimeout (line 4231) | _setTimeout(handler, timeout) {
    method _isWithActiveTrigger (line 4236) | _isWithActiveTrigger() {
    method _getConfig (line 4240) | _getConfig(config) {
    method _configAfterMerge (line 4261) | _configAfterMerge(config) {
    method _getDelegateConfig (line 4282) | _getDelegateConfig() {
    method _disposePopper (line 4299) | _disposePopper() {
  class Popover (line 4351) | class Popover extends Tooltip {
    method Default (line 4353) | static get Default() {
    method DefaultType (line 4357) | static get DefaultType() {
    method NAME (line 4361) | static get NAME() {
    method jQueryInterface (line 4365) | static jQueryInterface(config) {
    method _isWithContent (line 4381) | _isWithContent() {
    method _getContentForTemplate (line 4385) | _getContentForTemplate() {
    method _getContent (line 4392) | _getContent() {
  class ScrollSpy (line 4454) | class ScrollSpy extends BaseComponent {
    method constructor (line 4455) | constructor(element, config) {
    method Default (line 4471) | static get Default() {
    method DefaultType (line 4475) | static get DefaultType() {
    method NAME (line 4479) | static get NAME() {
    method jQueryInterface (line 4483) | static jQueryInterface(config) {
    method refresh (line 4499) | refresh() {
    method dispose (line 4515) | dispose() {
    method _configAfterMerge (line 4521) | _configAfterMerge(config) {
    method _maybeEnableSmoothScroll (line 4534) | _maybeEnableSmoothScroll() {
    method _getNewObserver (line 4563) | _getNewObserver() {
    method _observerCallback (line 4572) | _observerCallback(entries) {
    method _initializeTargetsAndObservables (line 4613) | _initializeTargetsAndObservables() {
    method _process (line 4634) | _process(target) {
    method _activateParents (line 4651) | _activateParents(target) {
    method _clearActiveClass (line 4667) | _clearActiveClass(parent) {
  class Tab (line 4737) | class Tab extends BaseComponent {
    method constructor (line 4738) | constructor(element) {
    method NAME (line 4754) | static get NAME() {
    method jQueryInterface (line 4758) | static jQueryInterface(config) {
    method show (line 4774) | show() {
    method _activate (line 4801) | _activate(element, relatedElem) {
    method _deactivate (line 4830) | _deactivate(element, relatedElem) {
    method _keydown (line 4860) | _keydown(event) {
    method _getChildren (line 4879) | _getChildren() {
    method _getActiveElem (line 4884) | _getActiveElem() {
    method _setInitialAttributes (line 4888) | _setInitialAttributes(parent, children) {
    method _setInitialAttributesOnChild (line 4896) | _setInitialAttributesOnChild(child) {
    method _setInitialAttributesOnTargetPanel (line 4919) | _setInitialAttributesOnTargetPanel(child) {
    method _toggleDropDown (line 4933) | _toggleDropDown(element, open) {
    method _setAttributeIfNotExists (line 4953) | _setAttributeIfNotExists(element, attribute, value) {
    method _elemIsActive (line 4959) | _elemIsActive(elem) {
    method _getInnerElement (line 4963) | _getInnerElement(elem) {
    method _getOuterElement (line 4967) | _getOuterElement(elem) {
  class Toast (line 5045) | class Toast extends BaseComponent {
    method constructor (line 5046) | constructor(element, config) {
    method Default (line 5056) | static get Default() {
    method DefaultType (line 5060) | static get DefaultType() {
    method NAME (line 5064) | static get NAME() {
    method jQueryInterface (line 5068) | static jQueryInterface(config) {
    method show (line 5082) | show() {
    method hide (line 5113) | hide() {
    method dispose (line 5138) | dispose() {
    method isShown (line 5148) | isShown() {
    method _maybeScheduleHide (line 5152) | _maybeScheduleHide() {
    method _onInteraction (line 5166) | _onInteraction(event, isInteracting) {
    method _setListeners (line 5196) | _setListeners() {
    method _clearTimeout (line 5203) | _clearTimeout() {

FILE: src/main/resources/web/js/petite-vue.js
  function st (line 5) | function st(e, t) {
  function de (line 11) | function de(e) {
  function it (line 27) | function it(e) {
  function me (line 37) | function me(e) {
  function ct (line 46) | function ct(e, t) {
  function I (line 53) | function I(e, t) {
  function G (line 70) | function G(e, t) {
  function we (line 91) | function we(e, t) {
  class vt (line 116) | class vt {
    method constructor (line 117) | constructor(t, n = null, s) {
    method run (line 121) | run() {
    method stop (line 132) | stop() {
  function Oe (line 137) | function Oe(e) {
  function wt (line 145) | function wt(e, t) {
  function _t (line 153) | function _t(e) {
  function Et (line 160) | function Et() {
  function $t (line 164) | function $t() {
  function ke (line 168) | function ke() {
  function F (line 173) | function F(e, t, n) {
  function St (line 181) | function St() {
  function Ot (line 185) | function Ot(e, t) {
  function se (line 190) | function se(e, t, n, s, r, i) {
  function Te (line 214) | function Te(e, t) {
  function Rt (line 221) | function Rt() {
  function Me (line 239) | function Me(e = !1, t = !1) {
  function Ct (line 253) | function Ct(e = !1) {
  function jt (line 262) | function jt(e, t) {
  function Pt (line 269) | function Pt(e, t) {
  function It (line 274) | function It(e) {
  method set (line 279) | set(e, t) {
  method deleteProperty (line 281) | deleteProperty(e, t) {
  function Dt (line 286) | function Dt(e) {
  function Vt (line 301) | function Vt(e) {
  function D (line 305) | function D(e) {
  function Ht (line 309) | function Ht(e) {
  function Pe (line 313) | function Pe(e, t, n, s, r) {
  function Lt (line 323) | function Lt(e) {
  function j (line 327) | function j(e) {
  function re (line 332) | function re(e) {
  method set (line 649) | set(i, c, o, l) {
  class ue (line 658) | class ue {
    method constructor (line 659) | constructor(t, n, s = !1) {
    method el (line 670) | get el() {
    method insert (line 674) | insert(t, n = null) {
    method remove (line 683) | remove() {
    method teardown (line 692) | teardown() {
  method directive (line 708) | directive(s, r) {
  method mount (line 710) | mount(s) {
  method unmount (line 715) | unmount() {
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,675K chars).
[
  {
    "path": ".gitignore",
    "chars": 87,
    "preview": "downloads/\n.gradle/\ngradle/\nlog/\nout/\njre/\nbuild/\n.idea/workspace.xml\nconfig.json\n*.exe"
  },
  {
    "path": ".idea/artifacts/webapp_hardware_bridge_jar.xml",
    "chars": 6200,
    "preview": "<component name=\"ArtifactManager\">\n  <artifact name=\"webapp-hardware-bridge:jar\">\n    <output-path>$PROJECT_DIR$/out/art"
  },
  {
    "path": ".idea/compiler.xml",
    "chars": 756,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <annotationP"
  },
  {
    "path": ".idea/gradle.xml",
    "chars": 512,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersio"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "chars": 999,
    "preview": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project De"
  },
  {
    "path": ".idea/jarRepositories.xml",
    "chars": 839,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RemoteRepositoriesConfiguration\">\n    <r"
  },
  {
    "path": ".idea/misc.xml",
    "chars": 480,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ExternalStorageConfigurationManager\" ena"
  },
  {
    "path": ".idea/modules/webapp-hardware-bridge.iml",
    "chars": 342,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module version=\"4\">\n  <component name=\"ExternalSystem\" externalSystem=\"GRADLE\" e"
  },
  {
    "path": ".idea/modules/webapp-hardware-bridge.main.iml",
    "chars": 392,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module version=\"4\">\n  <component name=\"AdditionalModuleElements\">\n    <content u"
  },
  {
    "path": ".idea/modules.xml",
    "chars": 322,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n   "
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 167,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": "ADVANCED.md",
    "chars": 2844,
    "preview": "# Advanced Configurations\n\n## Authentication\n\n### Enable Authentication\n\nAuthentication is disabled by default, that any"
  },
  {
    "path": "BUILD.md",
    "chars": 940,
    "preview": "# Build Instructions\n\n## Build from source\n\n- JDK 21, [Eclipse Temurin 21](https://adoptium.net/en-GB/temurin/releases/)"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1004,
    "preview": "# Changelogs\n\n## From 0.x to 1.0.0\n\n- 1.0 is a major rewrite, while maintain compatibility with existing WebApps\n- Setti"
  },
  {
    "path": "CONFIGURATION.md",
    "chars": 3012,
    "preview": "# Configurations\n\n## Web/WebSocket Server\n\n### Bind\n\n- (Default) `127.0.0.1` \n- `127.0.0.1` for normal usage\n- `0.0.0.0`"
  },
  {
    "path": "HTTP_API.md",
    "chars": 505,
    "preview": "# HTTP APIs\n\nAll endpoints have CORS configured to allow requests from any origin.\n\nYou can get or update the current co"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2017 imTigger\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 2614,
    "preview": "# WebApp Hardware Bridge\n\n## Introduction\n\nWebApp Hardware Bridge made it possible for WebApps to perform silent print a"
  },
  {
    "path": "TROUBLESHOOT.md",
    "chars": 138,
    "preview": "# Troubleshoot\n\n- Configurator/GUI do not run? Install [vc_redist.x64.exe](https://www.microsoft.com/en-US/download/deta"
  },
  {
    "path": "build.gradle",
    "chars": 1613,
    "preview": "plugins {\n    id 'java'\n    id 'application'\n}\n\ngroup 'webapp-hardware-bridge'\nversion '1.0.1'\n\nsourceCompatibility = '2"
  },
  {
    "path": "demo/printer-advanced.htm",
    "chars": 3421,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta content=\"wi"
  },
  {
    "path": "demo/printer-annotation.htm",
    "chars": 2596,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta content=\"wi"
  },
  {
    "path": "demo/printer-basic.htm",
    "chars": 12148,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta content=\"wi"
  },
  {
    "path": "demo/serial-basic.html",
    "chars": 1686,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta content=\"wi"
  },
  {
    "path": "demo/serial-weigh.htm",
    "chars": 1210,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <!-- Required meta tags -->\n    <meta charset=\"utf-8\">\n    <meta content=\"wi"
  },
  {
    "path": "demo/websocket-printer.js",
    "chars": 1288,
    "preview": "function WebSocketPrinter(options) {\n    var defaults = {\n        url: \"ws://127.0.0.1:12212/printer\",\n        onConnect"
  },
  {
    "path": "demo/websocket-serial.js",
    "chars": 998,
    "preview": "function WebSocketSerial(options) {\n    var defaults = {\n        url: 'ws://127.0.0.1:12212/serial/DISPLAY',\n        onC"
  },
  {
    "path": "demo/websocket-weigh.js",
    "chars": 1366,
    "preview": "function WebSocketWeigh(options) {\n    var defaults = {\n        url: 'ws://127.0.0.1:12212/serial/WEIGH',\n        weight"
  },
  {
    "path": "gradlew",
    "chars": 8669,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2776,
    "preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "install.nsi",
    "chars": 3620,
    "preview": "; The name of the installer\nName \"WebApp Hardware Bridge\"\n\n; The file to write\nOutFile \"whb.exe\"\n\n; The default installa"
  },
  {
    "path": "settings.gradle",
    "chars": 43,
    "preview": "rootProject.name = 'webapp-hardware-bridge'"
  },
  {
    "path": "src/main/java/module-info.java",
    "chars": 949,
    "preview": "module tigerworkshop.webapphardwarebridge {\n    requires java.desktop;\n    requires com.fazecast.jSerialComm;\n    requir"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/Constants.java",
    "chars": 268,
    "preview": "package tigerworkshop.webapphardwarebridge;\n\npublic class Constants {\n    public static final String APP_NAME = \"WebApp "
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/GUI.java",
    "chars": 5573,
    "preview": "package tigerworkshop.webapphardwarebridge;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.lo"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/Server.java",
    "chars": 16306,
    "preview": "package tigerworkshop.webapphardwarebridge;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.faste"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/dtos/Config.java",
    "chars": 3338,
    "preview": "package tigerworkshop.webapphardwarebridge.dtos;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterx"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/dtos/NotificationDTO.java",
    "chars": 296,
    "preview": "package tigerworkshop.webapphardwarebridge.dtos;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Getter;\nimport lombok."
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/dtos/PrintServiceDTO.java",
    "chars": 245,
    "preview": "package tigerworkshop.webapphardwarebridge.dtos;\n\nimport lombok.AllArgsConstructor;\nimport lombok.NoArgsConstructor;\n\n@N"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/dtos/SerialPortDTO.java",
    "chars": 275,
    "preview": "package tigerworkshop.webapphardwarebridge.dtos;\n\nimport lombok.AllArgsConstructor;\nimport lombok.NoArgsConstructor;\n\n@N"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/dtos/VersionDTO.java",
    "chars": 264,
    "preview": "package tigerworkshop.webapphardwarebridge.dtos;\n\nimport lombok.AllArgsConstructor;\nimport lombok.NoArgsConstructor;\n\n@N"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServerInterface.java",
    "chars": 466,
    "preview": "package tigerworkshop.webapphardwarebridge.interfaces;\n\n\npublic interface WebSocketServerInterface {\n    void messageToS"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/interfaces/WebSocketServiceInterface.java",
    "chars": 334,
    "preview": "package tigerworkshop.webapphardwarebridge.interfaces;\n\npublic interface WebSocketServiceInterface {\n    void start();\n\n"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintDocument.java",
    "chars": 625,
    "preview": "package tigerworkshop.webapphardwarebridge.responses;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport lomb"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/responses/PrintResult.java",
    "chars": 333,
    "preview": "package tigerworkshop.webapphardwarebridge.responses;\n\nimport lombok.AllArgsConstructor;\nimport lombok.NoArgsConstructor"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/services/ConfigService.java",
    "chars": 1918,
    "preview": "package tigerworkshop.webapphardwarebridge.services;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport "
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/services/DocumentService.java",
    "chars": 4513,
    "preview": "package tigerworkshop.webapphardwarebridge.services;\n\nimport lombok.Getter;\nimport lombok.extern.log4j.Log4j2;\nimport or"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/utils/AnnotatedPrintable.java",
    "chars": 3315,
    "preview": "package tigerworkshop.webapphardwarebridge.utils;\n\nimport lombok.Data;\nimport lombok.extern.log4j.Log4j2;\n\nimport java.a"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/utils/CertificateGenerator.java",
    "chars": 5097,
    "preview": "package tigerworkshop.webapphardwarebridge.utils;\n\nimport lombok.extern.log4j.Log4j2;\nimport org.bouncycastle.asn1.x500."
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/utils/ImagePrintable.java",
    "chars": 836,
    "preview": "package tigerworkshop.webapphardwarebridge.utils;\n\nimport java.awt.*;\nimport java.awt.print.PageFormat;\nimport java.awt."
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/utils/ThreadUtil.java",
    "chars": 235,
    "preview": "package tigerworkshop.webapphardwarebridge.utils;\n\npublic class ThreadUtil {\n    public static void silentSleep(long dur"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/PrinterWebSocketService.java",
    "chars": 13449,
    "preview": "package tigerworkshop.webapphardwarebridge.websocketservices;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimpor"
  },
  {
    "path": "src/main/java/tigerworkshop/webapphardwarebridge/websocketservices/SerialWebSocketService.java",
    "chars": 6099,
    "preview": "package tigerworkshop.webapphardwarebridge.websocketservices;\n\nimport com.fasterxml.jackson.core.JsonProcessingException"
  },
  {
    "path": "src/main/resources/META-INF/MANIFEST.MF",
    "chars": 1423,
    "preview": "Manifest-Version: 1.0\nMain-Class: tigerworkshop.webapphardwarebridge.GUI\nClass-Path: commons-io-2.16.1.jar jetty-securit"
  },
  {
    "path": "src/main/resources/log4j2.xml",
    "chars": 818,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration status=\"warn\">\n    <appenders>\n        <Console name=\"Console\" tar"
  },
  {
    "path": "src/main/resources/web/css/bootstrap-grid.css",
    "chars": 82303,
    "preview": "/*!\n * Bootstrap Grid v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 2011-"
  },
  {
    "path": "src/main/resources/web/css/bootstrap-grid.rtl.css",
    "chars": 82378,
    "preview": "/*!\n * Bootstrap Grid v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 2011-"
  },
  {
    "path": "src/main/resources/web/css/bootstrap-reboot.css",
    "chars": 8431,
    "preview": "/*!\n * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 201"
  },
  {
    "path": "src/main/resources/web/css/bootstrap-reboot.rtl.css",
    "chars": 8428,
    "preview": "/*!\n * Bootstrap Reboot v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright 201"
  },
  {
    "path": "src/main/resources/web/css/bootstrap-utilities.css",
    "chars": 85362,
    "preview": "/*!\n * Bootstrap Utilities v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright "
  },
  {
    "path": "src/main/resources/web/css/bootstrap-utilities.rtl.css",
    "chars": 85227,
    "preview": "/*!\n * Bootstrap Utilities v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * Copyright "
  },
  {
    "path": "src/main/resources/web/css/bootstrap.css",
    "chars": 258512,
    "preview": "@charset \"UTF-8\";\n/*!\n * Bootstrap  v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * C"
  },
  {
    "path": "src/main/resources/web/css/bootstrap.rtl.css",
    "chars": 258097,
    "preview": "@charset \"UTF-8\";\n/*!\n * Bootstrap  v5.2.3 (https://getbootstrap.com/)\n * Copyright 2011-2022 The Bootstrap Authors\n * C"
  },
  {
    "path": "src/main/resources/web/index.html",
    "chars": 19615,
    "preview": "<!doctype html>\n<!--suppress JSUnusedGlobalSymbols, HtmlFormInputWithoutLabel, JSUnresolvedReference -->\n<html lang=\"en\""
  },
  {
    "path": "src/main/resources/web/js/bootstrap.bundle.js",
    "chars": 236917,
    "preview": "/*!\n  * Bootstrap v5.2.3 (https://getbootstrap.com/)\n  * Copyright 2011-2022 The Bootstrap Authors (https://github.com/t"
  },
  {
    "path": "src/main/resources/web/js/bootstrap.esm.js",
    "chars": 149033,
    "preview": "/*!\n  * Bootstrap v5.2.3 (https://getbootstrap.com/)\n  * Copyright 2011-2022 The Bootstrap Authors (https://github.com/t"
  },
  {
    "path": "src/main/resources/web/js/bootstrap.js",
    "chars": 166587,
    "preview": "/*!\n  * Bootstrap v5.2.3 (https://getbootstrap.com/)\n  * Copyright 2011-2022 The Bootstrap Authors (https://github.com/t"
  },
  {
    "path": "src/main/resources/web/js/petite-vue.js",
    "chars": 25917,
    "preview": "var tt = Object.defineProperty;\nvar nt = (e, t, n) => t in e ? tt(e, t, {enumerable: !0, configurable: !0, writable: !0,"
  }
]

About this extraction

This page contains the full source code of the imTigger/webapp-hardware-bridge GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (1.5 MB), approximately 425.2k tokens, and a symbol index with 1326 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!