Compare commits

..

1 Commits

Author SHA1 Message Date
Janis
295153e702 newer gh-pages actions version 2023-10-07 15:32:27 +02:00
26 changed files with 6194 additions and 98 deletions

14
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
},
}

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Bug report
about: Something isn't working for you
title: ''
labels: bug
assignees: ''
---
> Screenshots are welcome
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Example or explanation to reproduce the issue.
## Expected behavior
A clear and concise description of what you expected to happen.
**Your environment:**
- OS: [e.g. Windows 11]
- OBS version: [e.g. 29.1.3]
- Overlay version [e.g. v1.1.0]

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "monthly"

31
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Deploy to GitHub Pages on Release
on:
release:
types:
- created
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 120
}

1
CNAME
View File

@@ -1 +0,0 @@
teamspeak-overlay.tealfire.de

79
README.md Normal file
View File

@@ -0,0 +1,79 @@
# Teamspeak5-OBS-Overlay
This is an overlay for OBS to show the current talking clients in your Teamspeak 5 Channel.
This App uses the new "Remote Apps" feature of Teamspeak 5.
This overlay uses the [Teamspeak 5 Remote App API](https://github.com/DerTyp7/react-ts5-remote-app-api).
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/d0ab06f2-1a36-479d-826f-bd4bd3d405b7)
- [Teamspeak5-OBS-Overlay](#teamspeak5-obs-overlay)
- [Setup](#setup)
- [Customization (Parameters)](#customization-parameters)
- [Setup (Developer)](#setup-developer)
- [Common Issues](#common-issues)
- [The overlay is empty, but i'm connected to a Teamspeak 5 server](#the-overlay-is-empty-but-im-connected-to-a-teamspeak-5-server)
## Setup
1. Download the `ts5-overlay-{version}.html` of the latest release from [here](https://github.com/DerTyp7/ts5-obs-overlay/releases/latest)
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/04dc3a66-c493-429b-b4ae-44bade473ad6)
1.1. (optional) You can rename the file. Just remember using the new file name in the future instead of `ts5-overlay-{version}.html`
2. Go into the Teamspeak 5 Settings and enable "Remote Apps"
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/b31bc553-fde2-46ab-b07c-d3c81339cc7d)
3. Add a new Browser Source to your OBS Scene
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/0198b468-bb96-4b65-bdd4-3d6bb3ef7d25)
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/58ad399f-5344-456f-b243-6e267b489fd5)
4. Tick the checkbox "Local File" and select the downloaded `ts5-overlay-{version}.html`
4.1. Set the width and height to your desired size (e.g. 1920x1080 OR 1280x720)
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/5ad8ce69-645b-45e7-acc3-ce7ba8d7f8ab)
5. You should now receive a notification in Teamspeak 5 that the app is allowed to connect to your Teamspeak 5 client. Allow it. (If you don't get a notification, restart Teamspeak 5 and OBS -> try again)
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/40faa435-e128-415f-98eb-a9e8809e8f65)
### Customization (Parameters)
You can customize the overlay by adding parameters to the URL of the Browser Source.
1. Open your Browser Source settings
2. **Untick** the checkbox "Local File"
3. Add `file://` to the beginning of the URL
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/87985b4c-4397-4681-9635-239d1e382c24)
4. Start adding parameters like discribed below
Start by adding a `?` to the end of the URL and then add the parameters.
To add multiple parameters, you have to seperate them with a `&`.
Like this: `file://C:/Users/.../ts5-overlay-{version}.html?parameter1=value1&parameter2=value2`
Real example: `file://C:/Users/.../ts5-overlay-{version}.html?remoteAppPort=5899&hideNonTalking=true&clientLimit=5`
This is a list of all available parameters (all parameters are optional):
| Parameter | Description | Type | Default |
| ----------------- | ---------------------------------------- | ------- | --------------- |
| `remoteAppPort` | The port of the Teamspeak 5 remote app | number | `5899` |
| `hideNonTalking` | Hide all non-talking clients | boolean | `false` |
| `clientLimit` | Count of how many client should be shown | number | `0` (unlimited) |
| `showChannelName` | Display the channel name | boolean | `false` |
## Setup (Developer)
1. Clone this repository
2. Run `npm install`
3. To start the development server run `npm run dev`
## Common Issues
### The overlay is empty, but i'm connected to a Teamspeak 5 server
Sadly TeamSpeak5 does not give us any information about the current active server tab.
So we try currently use a workaround, where the active server tab is determined by looking on which server the your hardware input was unmuted the latest, since the non-active server tabs in TS5 usually mute the clients microphone.
However this workaround is not 100% accurate and can fail in some cases.
Possible fixes:
- Unmute and mute yourself in the active server tab (Just a normal unmute and mute, not the hardware mute)
- Reconnect to the TS5 server while the overlay is open

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -1,16 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="An OBS overlay for TeamSpeak5, so your audience can see who is talking. https://github.com/DerTyp7/ts5-obs-overlay" />
<meta name="author" content="DerTyp7" />
<link rel="icon" href="./logo.svg" type="image/x-icon" />
<title>TS5-OBS-Overlay</title>
<script type="module" crossorigin src="./assets/index-DXas0TfE.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-MKACXyL3.css">
</head>
<body>
<div id="root"></div>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TS5-OBS-Overlay</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="307.000000pt" height="339.000000pt" viewBox="0 0 307.000000 339.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,339.000000) scale(0.100000,-0.100000)"
fill="#008080" stroke="none">
<path d="M1297 3382 c-3 -4 -7 -61 -11 -127 -20 -341 -120 -616 -370 -1010
-158 -251 -306 -436 -331 -415 -7 6 -18 52 -24 103 -22 170 -77 377 -101 377
-5 0 -21 -38 -35 -86 -27 -89 -79 -213 -212 -504 -201 -438 -244 -657 -180
-901 52 -198 102 -290 232 -427 137 -145 345 -310 474 -376 l34 -18 -5 24 c-3
12 -18 63 -32 113 -64 213 -19 418 154 701 106 173 164 314 194 474 16 82 30
120 45 120 13 0 55 -59 94 -131 45 -83 81 -198 102 -329 9 -57 18 -106 20
-108 7 -7 122 231 144 298 34 104 49 202 63 403 6 103 15 186 20 185 4 -2 79
-72 167 -155 257 -247 392 -414 501 -625 97 -187 102 -205 107 -413 6 -223 -1
-265 -73 -418 -31 -65 -53 -121 -50 -124 7 -7 188 122 295 212 210 176 368
383 455 598 149 370 117 760 -98 1193 -110 220 -245 417 -434 633 l-39 45 -7
-183 c-5 -106 -14 -203 -22 -230 -14 -46 -80 -144 -99 -145 -14 -1 -59 92
-101 209 -121 342 -331 651 -579 854 -130 107 -287 203 -298 183z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

5611
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "ts5-obs-overlay",
"private": true,
"version": "1.2.3",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@types/node": "^20.4.5",
"jest": "^29.6.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"react-ts5-remote-app-api": "^1.1.1",
"sass": "^1.64.1"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.1",
"@vitejs/plugin-react-swc": "^3.0.0",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"typescript": "^5.1.6",
"vite": "^4.4.7",
"vite-plugin-singlefile": "^0.13.5"
}
}

38
src/App.tsx Normal file
View File

@@ -0,0 +1,38 @@
/* eslint-disable react-hooks/exhaustive-deps */
import "@styles/App.scss";
import { useSearchParams } from "react-router-dom";
import useTSRemoteApp, { IClient } from "react-ts5-remote-app-api";
import Viewer from "./Viewer";
export default function App() {
const [searchParams] = useSearchParams();
const { clients, activeConnectionId, currentChannel } = useTSRemoteApp({
remoteAppPort: parseInt(searchParams.get("remoteAppPort") ?? "5899"),
auth: {
identifier: "de.tealfire.obs",
version: "1.2.3",
name: "TS5 OBS Overlay",
description: "A OBS overlay for TS5 by DerTyp876",
},
logging: true,
});
return (
<div className="App">
<Viewer
showChannelName={searchParams.get("showChannelName") === "true"}
hideNonTalking={searchParams.get("hideNonTalking") === "true"}
clientLimit={searchParams.get("clientLimit") ? parseInt(searchParams.get("clientLimit") ?? "0") : 0}
clients={
clients.map((client) => {
if (client.channel?.id === currentChannel?.id && client.channel.connection.id === activeConnectionId) {
return client;
}
}) as IClient[]
}
channel={currentChannel}
/>
</div>
);
}

148
src/Viewer.tsx Normal file
View File

@@ -0,0 +1,148 @@
import "@styles/Viewer.scss";
import { IChannel, IClient } from "react-ts5-remote-app-api";
export default function Viewer({
clients,
channel,
showChannelName = false,
hideNonTalking = false,
clientLimit = 0,
}: {
clients: IClient[] | undefined;
channel: IChannel | undefined;
showChannelName?: boolean;
hideNonTalking?: boolean;
clientLimit?: number;
}) {
return (
<div className="viewer">
{showChannelName ? (
<div className="channelNameContainer">
<h3>{channel?.properties.name}</h3>
</div>
) : null}
{clients?.map((client, i) => {
//* Client limit
if (clientLimit != 0 && i >= clientLimit) {
return null;
}
if (client) {
//* Non-talking client
if (
hideNonTalking &&
(client.properties.inputMuted || client.properties.outputMuted || client.talkStatus == 0)
) {
return null;
}
//* Normal client
return (
<div className="client" key={`${client.id}-${client.channel?.connection.id}`}>
{client.properties.outputHardware == false ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>muted_hardware_output</title>
<g id="muted_hardware_output.svg">
<path
d="M116.62,39a4.78,4.78,0,0,1-1.59.3l-6.29,3.63a45.42,45.42,0,0,1-13.33,57.64,49.4,49.4,0,0,1-5.82,3.62c-1.06.57-2.2.92-3.19,1.49-1.7,1-2.77,2.13-2.77,4.18a4.57,4.57,0,0,0,4.54,4.54,5.33,5.33,0,0,0,1.84-.35,54.49,54.49,0,0,0,26.94-75c-.12,0-.22,0-.34,0M88.18,13.58a4.57,4.57,0,0,0-4.54,4.54c0,2.06,1.06,3.19,2.77,4.18,1,.57,2.13.92,3.19,1.49a49.4,49.4,0,0,1,5.82,3.62,45.68,45.68,0,0,1,8.19,7.78l7-4a2.63,2.63,0,0,1,1.11-.34A54.31,54.31,0,0,0,90,13.94a5.33,5.33,0,0,0-1.84-.35"
fill="#d8d8d8"
/>
<path
d="M59.46,71.4,32.77,86.81l19,19a4.51,4.51,0,0,0,3.19,1.35,4.57,4.57,0,0,0,4.54-4.54V71.4M54.92,20.88a4.51,4.51,0,0,0-3.19,1.35L28.12,45.85H9.54A4.57,4.57,0,0,0,5,50.38V77.62a4.57,4.57,0,0,0,4.54,4.54H22.26l37.2-21.48V25.42a4.57,4.57,0,0,0-4.54-4.54"
fill="#d8d8d8"
/>
<path
d="M85.11,56.6l-7.87,4.54A10,10,0,0,1,77.62,64c0,8.58-8.23,7.09-8.23,12.48A4.53,4.53,0,0,0,73.93,81a4,4,0,0,0,1.77-.35A18.13,18.13,0,0,0,86.69,64a18.34,18.34,0,0,0-1.59-7.4M73.93,47a4.52,4.52,0,0,0-4.54,4.54,3.92,3.92,0,0,0,1.08,2.8l8.76-5.06a16.14,16.14,0,0,0-3.52-1.93A4,4,0,0,0,73.93,47"
fill="#d8d8d8"
/>
<path
d="M100.87,47.49,93,52a27.15,27.15,0,0,1-8.36,33.87A36.79,36.79,0,0,1,79.25,89a4.54,4.54,0,0,0,1.84,8.72,5.24,5.24,0,0,0,1.77-.35,36.34,36.34,0,0,0,18-49.91M81,30.25A4.54,4.54,0,0,0,79.25,39a36.82,36.82,0,0,1,5.39,3.12,27,27,0,0,1,2.86,2.4l8.14-4.7A35.37,35.37,0,0,0,82.86,30.6,5.31,5.31,0,0,0,81,30.25"
fill="#d8d8d8"
/>
<path
d="M126.57,43.71,10.42,110.77a2.88,2.88,0,0,1-3.93-1.05L4.33,106A2.88,2.88,0,0,1,5.38,102L121.53,35A2.88,2.88,0,0,1,125.46,36l2.16,3.75A2.88,2.88,0,0,1,126.57,43.71Z"
fill="#c9070a"
/>
</g>
</svg>
) : client.properties.inputHardware == false ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>muted_hardware_input</title>
<g id="muted_hardware_input.svg">
<path
d="M88.62,54.15V64A24.69,24.69,0,0,1,64,88.62a25.26,25.26,0,0,1-8.38-1.46l-7.39,7.39A34,34,0,0,0,64,98.46,34.5,34.5,0,0,0,98.46,64V54.15a4.92,4.92,0,1,1,9.85,0V64a44.31,44.31,0,0,1-39.38,44v10.15H88.62a4.92,4.92,0,0,1,0,9.85H39.38a4.92,4.92,0,1,1,0-9.85H59.08V108A43.3,43.3,0,0,1,41,101.77L21.46,121.31a2.46,2.46,0,0,1-3.54,0L11.62,115a2.46,2.46,0,0,1,0-3.54l94.92-94.92a2.46,2.46,0,0,1,3.54,0l6.31,6.31a2.46,2.46,0,0,1,0,3.54ZM22.92,80.46A43.3,43.3,0,0,1,19.69,64V54.15a4.92,4.92,0,1,1,9.85,0V64a35.94,35.94,0,0,0,1.15,8.69ZM39.38,64V24.62a24.62,24.62,0,0,1,47.77-8.38Z"
fill="#d8d8d8"
/>
<rect
x="-5.93"
y="61.89"
width="139.87"
height="14.02"
rx="2.87"
ry="2.87"
transform="translate(-29.97 65.43) rotate(-45)"
fill="#c9070a"
/>
</g>
</svg>
) : client.properties.outputMuted ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>muted_output</title>
<g id="muted_output">
<path
d="M116.62,39a4.78,4.78,0,0,1-1.59.3l-6.29,3.63a45.42,45.42,0,0,1-13.33,57.64,49.4,49.4,0,0,1-5.82,3.62c-1.06.57-2.2.92-3.19,1.49-1.7,1-2.77,2.13-2.77,4.18a4.57,4.57,0,0,0,4.54,4.54,5.33,5.33,0,0,0,1.84-.35,54.49,54.49,0,0,0,26.94-75c-.12,0-.22,0-.34,0M88.18,13.58a4.57,4.57,0,0,0-4.54,4.54c0,2.06,1.06,3.19,2.77,4.18,1,.57,2.13.92,3.19,1.49a49.4,49.4,0,0,1,5.82,3.62,45.68,45.68,0,0,1,8.19,7.78l7-4a2.63,2.63,0,0,1,1.11-.34A54.31,54.31,0,0,0,90,13.94a5.33,5.33,0,0,0-1.84-.35"
fill="#c9070a"
/>
<path
d="M59.46,71.4,32.77,86.81l19,19a4.51,4.51,0,0,0,3.19,1.35,4.57,4.57,0,0,0,4.54-4.54V71.4M54.92,20.88a4.51,4.51,0,0,0-3.19,1.35L28.11,45.85H9.53A4.57,4.57,0,0,0,5,50.38V77.62a4.57,4.57,0,0,0,4.54,4.54H22.25l37.2-21.48V25.42a4.57,4.57,0,0,0-4.54-4.54"
fill="#c9070a"
/>
<path
d="M85.1,56.6l-7.87,4.54A10,10,0,0,1,77.61,64c0,8.58-8.23,7.09-8.23,12.48A4.53,4.53,0,0,0,73.92,81a4,4,0,0,0,1.77-.35A18.13,18.13,0,0,0,86.69,64a18.34,18.34,0,0,0-1.59-7.4M73.92,47a4.52,4.52,0,0,0-4.54,4.54,3.92,3.92,0,0,0,1.08,2.8l8.76-5.06a16.14,16.14,0,0,0-3.52-1.93A4,4,0,0,0,73.92,47"
fill="#c9070a"
/>
<path
d="M100.87,47.49,93,52a27.15,27.15,0,0,1-8.36,33.87A36.79,36.79,0,0,1,79.24,89a4.54,4.54,0,0,0,1.84,8.72,5.24,5.24,0,0,0,1.77-.35,36.34,36.34,0,0,0,18-49.91M81,30.25A4.54,4.54,0,0,0,79.24,39a36.82,36.82,0,0,1,5.39,3.12,27,27,0,0,1,2.86,2.4l8.14-4.7A35.37,35.37,0,0,0,82.86,30.6,5.31,5.31,0,0,0,81,30.25"
fill="#c9070a"
/>
<path
d="M126.57,43.71,10.42,110.77a2.88,2.88,0,0,1-3.93-1.05L4.33,106A2.88,2.88,0,0,1,5.38,102L121.53,35A2.88,2.88,0,0,1,125.46,36l2.16,3.75A2.88,2.88,0,0,1,126.57,43.71Z"
fill="#c9070a"
/>
</g>
</svg>
) : client.properties.inputMuted ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>muted_input</title>
<g id="muted_input.svg">
<path
d="M88.62,54.15V64A24.69,24.69,0,0,1,64,88.62a25.26,25.26,0,0,1-8.38-1.46l-7.39,7.39A34,34,0,0,0,64,98.46,34.5,34.5,0,0,0,98.46,64V54.15a4.92,4.92,0,1,1,9.85,0V64a44.31,44.31,0,0,1-39.38,44v10.15H88.62a4.92,4.92,0,0,1,0,9.85H39.38a4.92,4.92,0,1,1,0-9.85H59.08V108A43.3,43.3,0,0,1,41,101.77L21.46,121.31a2.46,2.46,0,0,1-3.54,0L11.62,115a2.46,2.46,0,0,1,0-3.54l94.92-94.92a2.46,2.46,0,0,1,3.54,0l6.31,6.31a2.46,2.46,0,0,1,0,3.54ZM22.92,80.46A43.3,43.3,0,0,1,19.69,64V54.15a4.92,4.92,0,1,1,9.85,0V64a35.94,35.94,0,0,0,1.15,8.69ZM39.38,64V24.62a24.62,24.62,0,0,1,47.77-8.38Z"
fill="#c9070a"
/>
</g>
</svg>
) : client.talkStatus == 1 ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>player_on_v2</title>
<g id="player_on_v2.svg">
<path d="M64,128a64,64,0,1,1,64-64A64,64,0,0,1,64,128Z" fill="#00b4df" />
</g>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>player_off_v2</title>
<g id="player_off_v2.svg">
<path d="M64,128a64,64,0,1,1,64-64A64,64,0,0,1,64,128Z" fill="#3e4f5e" />
</g>
</svg>
)}
<p>{client.properties.nickname}</p>
</div>
);
} else {
return <div key={Math.random()}></div>;
}
})}
</div>
);
}

11
src/main.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App.tsx";
import "@styles/index.scss";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<BrowserRouter>
<App />
</BrowserRouter>
);

48
src/styles/Viewer.scss Normal file
View File

@@ -0,0 +1,48 @@
.viewer {
display: flex;
flex-direction: column;
gap: 0 0;
padding: 1rem;
.channelNameContainer {
display: flex;
align-items: center;
margin-bottom: 20px;
h3 {
font-size: 1.7rem;
font-weight: 500;
margin: 0;
background-color: rgba(47, 49, 54, 0.5);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
color: white;
}
}
.client {
display: flex;
flex-direction: row;
gap: 0 0;
align-items: center;
margin: 0.5rem 0;
svg {
width: 2.8rem;
height: 2.8rem;
margin-right: 0.5rem;
}
p {
margin: 0;
color: white;
background-color: rgba(47, 49, 54, 0.5);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
// ellipsis after 22 characters
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 20ch;
}
}
}

5
src/styles/index.scss Normal file
View File

@@ -0,0 +1,5 @@
* {
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
font-size: 3rem;
}

31
src/utils/logger.tsx Normal file
View File

@@ -0,0 +1,31 @@
export default class Logger {
// Log message to the console
public static log(message: string, data: object | null = null): void {
console.log(`[Log] %c${message}`.trim(), "color: gray", data ?? "");
}
// Log warning to the console
public static warn(message: string, data: object | null = null): void {
console.warn(`%c${message}`.trim(), data ?? "");
}
// Log error to the console
public static error(message: string, data: object | null = null): void {
console.error(`%c${message}`.trim(), data ?? "");
}
// Log message received from the websocket to the console
public static wsReceived(data: object, message: string | undefined = undefined): void {
console.log(`%c[WS Recieved] ${message ?? ""}`.trim(), "color: #8258c7", data);
}
// Log message sent to the websocket to the console
public static wsSent(data: object, message: string | undefined = undefined): void {
console.log(`%c[WS Sent] ${message ?? ""}`.trim(), "color: #4eb570", data);
}
// Log message to the console with a timestamp
public static ts(message: string, data: object | null = null): void {
console.log(`%c[TS] ${message}`.trim(), "color: #2e6bc7", data ?? "");
}
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

36
tsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@assets/*": ["src/assets/*"],
"@styles/*": ["src/styles/*"],
"@utils/*": ["src/utils/*"],
"@interfaces/*": ["src/interfaces/*"],
"@handlers/*": ["src/handlers/*"]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

25
vite.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { viteSingleFile } from "vite-plugin-singlefile"
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
base: "./",
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@assets': path.resolve(__dirname, './src/assets'),
'@components': path.resolve(__dirname, './src/components'),
'@handlers': path.resolve(__dirname, './src/handlers'),
'@interfaces': path.resolve(__dirname, './src/interfaces'),
'@utils': path.resolve(__dirname, './src/utils'),
'@styles': path.resolve(__dirname, './src/styles'),
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
},
plugins: [react(), viteSingleFile({ useRecommendedBuildConfig: false })],
})