Compare commits

..

14 Commits

Author SHA1 Message Date
DerTyp7
7f5f253c21 Add SVG icons and integrate vite-plugin-svgr for improved asset handling 2025-01-24 16:14:03 +01:00
Janis
9c94a351ee Add option to align right (#124) 2025-01-24 14:39:17 +01:00
Janis
9de39752f3 Merge branch 'main' into dev 2025-01-24 13:41:41 +01:00
DerTyp7
2499582f66 Update header title in Generator component to reflect TeamSpeak branding 2025-01-24 13:39:35 +01:00
DerTyp7
ad26dd62ad Update Viewer component to reflect TeamSpeak branding in names and descriptions 2025-01-24 13:39:06 +01:00
DerTyp7
0a026baa9b Rename project to teamspeak-obs-overlay and update related references 2025-01-24 13:37:20 +01:00
DerTyp7
a2b8f3620a Update package-lock.json to version 2.2.0 and replace react-ts5-remote-app-api with react-teamspeak-remote-app-api 2025-01-24 13:32:42 +01:00
DerTyp7
06b940b7c9 Fix README and package.json to update API name from TeamSpeak5 to TeamSpeak 2025-01-24 13:31:50 +01:00
DerTyp7
c75d81eb43 Update dependencies and version to 2.2.0 2025-01-24 01:53:56 +01:00
Janis
529d1acbb6 Merge pull request #113 from DerTyp7/dependabot/npm_and_yarn/dev/typescript-eslint/eslint-plugin-7.0.0
Bump @typescript-eslint/eslint-plugin from 6.20.0 to 7.0.0
2025-01-24 00:08:32 +01:00
dependabot[bot]
e7e295cad1 Bump @types/jest from 29.5.11 to 29.5.12
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.11 to 29.5.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-24 00:06:12 +01:00
Janis
10e8752bca Merge pull request #111 from DerTyp7/dependabot/npm_and_yarn/dev/types/jest-29.5.12
Bump @types/jest from 29.5.11 to 29.5.12
2025-01-24 00:02:22 +01:00
dependabot[bot]
6eb5785bfd Bump @typescript-eslint/eslint-plugin from 6.20.0 to 7.0.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.20.0 to 7.0.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.0.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 01:54:49 +00:00
dependabot[bot]
6c912c37e4 Bump @types/jest from 29.5.11 to 29.5.12
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.11 to 29.5.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 01:54:09 +00:00
21 changed files with 2835 additions and 1314 deletions

View File

@@ -1,7 +1,7 @@
{ {
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"printWidth": 180, "printWidth": 120,
"singleQuote": false, "singleQuote": false,
"semi": true "semi": true
} }

View File

@@ -3,7 +3,7 @@
This is an overlay for OBS to show the current talking clients in your TeamSpeak5 Channel. This is an overlay for OBS to show the current talking clients in your TeamSpeak5 Channel.
This App uses the new "Remote Apps" feature of TeamSpeak5. This App uses the new "Remote Apps" feature of TeamSpeak5.
This overlay uses the [TeamSpeak5 Remote App API](https://github.com/DerTyp7/react-ts5-remote-app-api). This overlay uses the [TeamSpeak Remote App API](https://github.com/DerTyp7/react-teamspeak-remote-app-api).
![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/d0ab06f2-1a36-479d-826f-bd4bd3d405b7) ![image](https://github.com/DerTyp7/ts5-obs-overlay/assets/76851529/d0ab06f2-1a36-479d-826f-bd4bd3d405b7)

View File

@@ -3,10 +3,13 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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="description"
content="An OBS overlay for TeamSpeak5/6, so your audience can see who is talking. https://github.com/DerTyp7/teamspeak-obs-overlay"
/>
<meta name="author" content="DerTyp7" /> <meta name="author" content="DerTyp7" />
<link rel="icon" href="/logo.svg" type="image/x-icon" /> <link rel="icon" href="/logo.svg" type="image/x-icon" />
<title>TS5-OBS-Overlay</title> <title>TeamSpeak-OBS-Overlay</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

3699
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,20 @@
{ {
"name": "ts5-obs-overlay", "name": "teamspeak-obs-overlay",
"private": false, "private": false,
"version": "2.1.1", "version": "2.2.0",
"description": "Overlay for OBS to show the current talking clients in your TeamSpeak 5 Channel", "description": "Overlay for OBS to show the current talking clients in your TeamSpeak Channel",
"author": "DerTyp7", "author": "DerTyp7",
"homepage": "https://dertyp7.github.io/ts5-obs-overlay/#", "homepage": "https://dertyp7.github.io/teamspeak-obs-overlay/#",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/DerTyp7/ts5-obs-overlay" "url": "https://github.com/DerTyp7/teamspeak-obs-overlay"
}, },
"keywords": [ "keywords": [
"ts5", "ts5",
"ts6",
"teamspeak6",
"teamspeak", "teamspeak",
"teamspeak5",
"overlay", "overlay",
"remote app", "remote app",
"obs", "obs",
@@ -20,7 +23,7 @@
"vite" "vite"
], ],
"bugs": { "bugs": {
"url": "https://github.com/DerTyp7/ts5-obs-overlay/issues" "url": "https://github.com/DerTyp7/teamspeak-obs-overlay/issues"
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -30,24 +33,25 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@types/node": "^20.8.3", "@types/node": "^22.10.10",
"react": "^18.2.0", "react": "^19.0.0",
"react-dom": "^18.2.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^7.1.3",
"react-ts5-remote-app-api": "^1.1.1", "react-teamspeak-remote-app-api": "^2.0.0",
"sass": "^1.68.0" "sass": "^1.83.4"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.2", "@types/jest": "^29.5.14",
"@types/react": "^18.2.35", "@types/react": "^19.0.8",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^19.0.3",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^6.9.1", "@typescript-eslint/parser": "^8.21.0",
"@vitejs/plugin-react-swc": "^3.6.0", "@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^8.56.0", "eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.18",
"typescript": "^5.2.2", "typescript": "^5.7.3",
"vite": "^4.5.2" "vite": "^6.0.11",
"vite-plugin-svgr": "^4.3.0"
} }
} }

View File

@@ -1,6 +1,6 @@
import "@styles/App.scss"; import "@styles/App.scss";
import { Navigate, Route, Routes, useSearchParams } from "react-router-dom"; import { Route, Routes, useSearchParams } from "react-router-dom";
import Viewer from "./Viewer"; import Viewer from "./Viewer";
import Generator from "./Generator"; import Generator from "./Generator";
@@ -17,6 +17,8 @@ export default function App() {
showChannelName={searchParams.get("showChannelName") === "true"} showChannelName={searchParams.get("showChannelName") === "true"}
hideNonTalking={searchParams.get("hideNonTalking") === "true"} hideNonTalking={searchParams.get("hideNonTalking") === "true"}
clientLimit={searchParams.get("clientLimit") ? parseInt(searchParams.get("clientLimit") ?? "0") : 0} clientLimit={searchParams.get("clientLimit") ? parseInt(searchParams.get("clientLimit") ?? "0") : 0}
alignRight={searchParams.get("alignRight") === "true"}
showTsAvatar={searchParams.get("showTsAvatar") === "true"}
/> />
} }
/> />

View File

@@ -1,6 +1,7 @@
import React, { ChangeEvent, useRef, useState, useEffect } from "react"; import { ChangeEvent, useRef, useState, useEffect } from "react";
import "@styles/Generator.scss"; import "@styles/Generator.scss";
import Viewer from "./Viewer"; import Viewer from "./Viewer";
import { url } from "inspector";
export default function Generator() { export default function Generator() {
// State variables // State variables
@@ -9,13 +10,15 @@ export default function Generator() {
const [remoteAppPort, setRemoteAppPort] = useState(5899); const [remoteAppPort, setRemoteAppPort] = useState(5899);
const [showChannelName, setShowChannelName] = useState(true); const [showChannelName, setShowChannelName] = useState(true);
const [alignRight, setAlignRight] = useState(false);
const [hideNonTalking, setHideNonTalking] = useState(false); const [hideNonTalking, setHideNonTalking] = useState(false);
const [clientLimit, setClientLimit] = useState(0); const [clientLimit, setClientLimit] = useState(0);
const [showTsAvatar, setShowTsAvatar] = useState(false);
// Effect to generate URL when dependencies change // Effect to generate URL when dependencies change
useEffect(() => { useEffect(() => {
generateUrl(); generateUrl();
}, [remoteAppPort, showChannelName, hideNonTalking, clientLimit]); }, [remoteAppPort, showChannelName, hideNonTalking, clientLimit, alignRight, showTsAvatar]);
// Function to generate the output URL // Function to generate the output URL
function generateUrl() { function generateUrl() {
@@ -26,6 +29,8 @@ export default function Generator() {
url.searchParams.set("showChannelName", showChannelName.toString()); url.searchParams.set("showChannelName", showChannelName.toString());
url.searchParams.set("hideNonTalking", hideNonTalking.toString()); url.searchParams.set("hideNonTalking", hideNonTalking.toString());
url.searchParams.set("clientLimit", clientLimit.toString()); url.searchParams.set("clientLimit", clientLimit.toString());
url.searchParams.set("alignRight", alignRight.toString());
url.searchParams.set("showTsAvatar", showTsAvatar.toString());
// url.hash function always sets the hash to the end of the URL, so we have to replace the question mark with a hash // url.hash function always sets the hash to the end of the URL, so we have to replace the question mark with a hash
// gh-pages needs the hash to be between the base URL and the search params // gh-pages needs the hash to be between the base URL and the search params
@@ -53,7 +58,7 @@ export default function Generator() {
<div className="generator"> <div className="generator">
{/* Header */} {/* Header */}
<div className="headline"> <div className="headline">
<h1>TS5-OBS-Overlay Generator</h1> <h1>TeamSpeak-OBS-Overlay Generator</h1>
<h4>by DerTyp7</h4> <h4>by DerTyp7</h4>
</div> </div>
@@ -62,7 +67,9 @@ export default function Generator() {
<p>1. Customize your settings</p> <p>1. Customize your settings</p>
<p>2. Copy the generated URL</p> <p>2. Copy the generated URL</p>
<p>3. Paste the URL into the BrowserSource URL field in OBS</p> <p>3. Paste the URL into the BrowserSource URL field in OBS</p>
<a href="https://github.com/DerTyp7/ts5-obs-overlay#detailed-instructions">Click here for detailed instructions</a> <a href="https://github.com/DerTyp7/ts5-obs-overlay#detailed-instructions">
Click here for detailed instructions
</a>
</div> </div>
{/* Output Section */} {/* Output Section */}
@@ -89,27 +96,49 @@ export default function Generator() {
<section> <section>
{/* Show Channel Name Option */} {/* Show Channel Name Option */}
<div className="option" onClick={() => setShowChannelName(!showChannelName)}> <div className="option" onClick={() => setShowChannelName(!showChannelName)}>
<input type="checkbox" checked={showChannelName} /> <input type="checkbox" defaultChecked={showChannelName} />
<label>Show channel name</label> <label>Show channel name</label>
</div> </div>
{/* Hide Non-Talking Clients Option */} {/* Hide Non-Talking Clients Option */}
<div className="option" onClick={() => setHideNonTalking(!hideNonTalking)}> <div className="option" onClick={() => setHideNonTalking(!hideNonTalking)}>
<input type="checkbox" checked={hideNonTalking} /> <input type="checkbox" defaultChecked={hideNonTalking} />
<label>Hide non talking clients</label> <label>Hide non talking clients</label>
</div> </div>
{/* Alignment Option */}
<div className="option" onClick={() => setAlignRight(!alignRight)}>
<input type="checkbox" defaultChecked={alignRight} />
<label>Align Right</label>
</div>
{/* Show TeamSpeak Avatar */}
<div className="option" onClick={() => setShowTsAvatar(!showTsAvatar)}>
<input type="checkbox" defaultChecked={showTsAvatar} />
<label>Show TeamSpeak Avatar</label>
</div>
</section> </section>
<section> <section>
{/* Client Limit Option */} {/* Client Limit Option */}
<div className="option"> <div className="option">
<input type="number" value={clientLimit} min={0} onChange={(e: ChangeEvent<HTMLInputElement>) => setClientLimit(parseInt(e.target.value))} /> <input
type="number"
value={clientLimit}
min={0}
onChange={(e: ChangeEvent<HTMLInputElement>) => setClientLimit(parseInt(e.target.value))}
/>
<label>Client Limit</label> <label>Client Limit</label>
</div> </div>
{/* RemoteApp-Port Option */} {/* RemoteApp-Port Option */}
<div className="option"> <div className="option">
<input type="number" value={remoteAppPort} min={0} onChange={(e: ChangeEvent<HTMLInputElement>) => setRemoteAppPort(parseInt(e.target.value))} /> <input
type="number"
value={remoteAppPort}
min={0}
onChange={(e: ChangeEvent<HTMLInputElement>) => setRemoteAppPort(parseInt(e.target.value))}
/>
<label>RemoteApp-Port</label> <label>RemoteApp-Port</label>
</div> </div>
</section> </section>
@@ -118,7 +147,7 @@ export default function Generator() {
{/* Preview */} {/* Preview */}
<div className="preview"> <div className="preview">
<Viewer remoteAppPort={remoteAppPort} showChannelName={showChannelName} hideNonTalking={hideNonTalking} clientLimit={clientLimit} /> <iframe src={outputUrl}></iframe>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,24 +1,29 @@
import "@styles/Viewer.scss"; import "@styles/Viewer.scss";
import useTSRemoteApp, { IClient } from "react-ts5-remote-app-api"; import useTSRemoteApp, { IClient } from "react-teamspeak-remote-app-api";
import TalkStatus from "@/components/TalkStatus";
export default function Viewer({ export default function Viewer({
remoteAppPort = 5899, remoteAppPort = 5899,
showChannelName = false, showChannelName = false,
hideNonTalking = false, hideNonTalking = false,
clientLimit = 0, clientLimit = 0,
alignRight = false,
showTsAvatar = false,
}: { }: {
remoteAppPort?: number; remoteAppPort?: number;
showChannelName?: boolean; showChannelName?: boolean;
hideNonTalking?: boolean; hideNonTalking?: boolean;
clientLimit?: number; clientLimit?: number;
alignRight?: boolean;
showTsAvatar?: boolean;
}) { }) {
const { clients, activeConnectionId, currentChannel } = useTSRemoteApp({ const { clients, activeConnectionId, currentChannel } = useTSRemoteApp({
remoteAppPort: remoteAppPort, remoteAppPort: remoteAppPort,
auth: { auth: {
identifier: "de.tealfire.obs", identifier: "de.tealfire.obs",
version: "2.1.1", version: "2.2.0",
name: "TS5 OBS Overlay", name: "TeamSpeak OBS Overlay",
description: "A OBS overlay for TS5 by DerTyp7", description: "A OBS overlay for TeamSpeak by DerTyp7",
}, },
logging: true, logging: true,
}); });
@@ -30,7 +35,7 @@ export default function Viewer({
}) as IClient[]; }) as IClient[];
return ( return (
<div className="viewer"> <div className={`viewer ${alignRight ? "viewer--align-right" : ""}`}>
{showChannelName && currentChannel ? ( {showChannelName && currentChannel ? (
<div className="channelNameContainer"> <div className="channelNameContainer">
<h1>{currentChannel?.properties.name}</h1> <h1>{currentChannel?.properties.name}</h1>
@@ -44,101 +49,20 @@ export default function Viewer({
if (client) { if (client) {
//* Non-talking client //* Non-talking client
if (hideNonTalking && (client.properties.inputMuted || client.properties.outputMuted || client.talkStatus == 0)) { if (
hideNonTalking &&
(client.properties.inputMuted || client.properties.outputMuted || client.talkStatus == 0)
) {
return null; return null;
} }
//* Normal client //* Normal client
return ( return (
<div className="client" key={`${client.id}-${client.channel?.connection.id}`}> <div
{client.properties.outputHardware == false ? ( className={`client ${alignRight ? "client--align-right" : ""}`}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"> key={`${client.id}-${client.channel?.connection.id}`}
<title>muted_hardware_output</title> >
<g id="muted_hardware_output.svg"> <TalkStatus client={client} showTsAvatar={showTsAvatar} />
<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> <p>{client.properties.nickname}</p>
</div> </div>
); );
@@ -151,11 +75,11 @@ export default function Viewer({
<h4>Overlay couldn't connect to the client:</h4> <h4>Overlay couldn't connect to the client:</h4>
<br /> <br />
<br /> <br />
<h5>1. Make sure to accept the overlay in your TS5-Client via the notifications</h5> <h5>1. Make sure to accept the overlay in your TeamSpeak-Client via the notifications</h5>
<br /> <br />
<h5>2. Enable remote apps inside the the TS5-Settings</h5> <h5>2. Enable remote apps inside the the TeamSpeak-Settings</h5>
<br /> <br />
<h5>3. Make sure to match the configuration port with the port in the TS5 remote app settings</h5> <h5>3. Make sure to match the configuration port with the port in the TeamSpeak remote app settings</h5>
<br /> <br />
<h5>4. Refresh this page/BrowserSource (Select BrowserSource & click "Refresh" in OBS)</h5> <h5>4. Refresh this page/BrowserSource (Select BrowserSource & click "Refresh" in OBS)</h5>
<br /> <br />

View File

@@ -0,0 +1,19 @@
<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>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 748 B

View File

@@ -0,0 +1,25 @@
<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>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,25 @@
<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>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -0,0 +1,47 @@
import "@styles/TalkStatus.scss";
import { IClient } from "react-teamspeak-remote-app-api";
import MutedHardwareOutputSvg from "@assets/icons/muted_hardware_output.svg?react";
import InputHardwareSvg from "@assets/icons/input_hardware.svg?react";
import OutputMutedSvg from "@assets/icons/output_muted.svg?react";
import InputMutedSvg from "@assets/icons/input_muted.svg?react";
import TalkingSvg from "@assets/icons/talk.svg?react";
import NotTalkingSvg from "@assets/icons/not_talk.svg?react";
// TODO outsource svg to files
export default function TalkStatus({ client, showTsAvatar }: { client: IClient; showTsAvatar: boolean }) {
function getTeamSpeakAvatarUrl(): string {
// MyTeamSpeak avatar property looks like this:
// 1,https://storage.googleapis.com/ts-sys-myts-avatars/d4864285-0b77-4f2d-b0c5-1ae585e66ee5/258947109;2,https://storage.googleapis.com/ts-sys-myts-avatars/d4864285-0b77-4f2d-b0c5-1ae585e66ee5/71073868;4,https://storage.googleapis.com/ts-sys-myts-avatars/d4864285-0b77-4f2d-b0c5-1ae585e66ee5/1260429936
// 2, is the avatar we want to use. The other ones are away and busy avatars.
return (
client.properties.myteamspeakAvatar.split("2,")[1] ??
client.properties.myteamspeakAvatar.split(";")[0].split(",")[1]
);
}
return (
<div className={`talkStatus ${showTsAvatar ? "talkStatus--avatar" : ""} `}>
{showTsAvatar ? (
<div className={`avatar ${client.talkStatus === 1 ? "avatar--talk" : ""}`}>
<img src={getTeamSpeakAvatarUrl()} alt="" />
</div>
) : null}
{client.properties.outputHardware === false ? (
<MutedHardwareOutputSvg />
) : client.properties.inputHardware === false ? (
<InputHardwareSvg />
) : client.properties.outputMuted ? (
<OutputMutedSvg />
) : client.properties.inputMuted ? (
<InputMutedSvg />
) : client.talkStatus === 1 && !showTsAvatar ? (
<TalkingSvg />
) : !showTsAvatar ? (
<NotTalkingSvg />
) : (
<svg></svg>
)}
</div>
);
}

View File

@@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import "@styles/index.scss"; import "@styles/index.scss";

View File

@@ -202,27 +202,17 @@ $breakpoint-3: 600px;
// Preview styles // Preview styles
.preview { .preview {
display: flex;
flex: 1; flex: 1;
border: 2px solid #31f39973;
// Viewer styles (see src/styles/Viewer.scss)
.viewer {
background-image: url("/images/viewer_example_background.png"); background-image: url("/images/viewer_example_background.png");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
min-height: 500px; border: 2px solid #31f39973;
h1 {
font-size: 2rem;
}
.client {
svg {
width: 2rem;
}
p { iframe {
font-size: 2rem; width: 100%;
} min-height: 500px;
} border: none;
} }
} }
} }

View File

@@ -0,0 +1,42 @@
.talkStatus {
display: flex;
align-items: center;
justify-content: center;
&--avatar {
margin-right: 4rem;
svg {
width: 6rem !important;
margin-left: -9.5rem;
}
}
svg {
width: 5rem;
aspect-ratio: 1/1;
margin-right: 0.5rem;
filter: drop-shadow(0 0 0.75rem rgba(15, 15, 15, 0.1));
}
}
.avatar {
$border-radius: 100rem;
$border-width: 0.7rem;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1/1;
border: $border-width solid transparent;
border-radius: $border-radius;
img {
width: 10rem;
border-radius: $border-radius;
}
&--talk {
border: $border-width solid rgb(0, 195, 255);
}
}

View File

@@ -5,17 +5,22 @@
.viewer { .viewer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5rem; padding: 0.3rem;
gap: 0.3rem;
&--align-right {
align-items: flex-end;
}
h1 { h1 {
font-size: 5vw; font-size: 4.5rem;
} }
h1, h1,
p { p {
background-color: #2f313680; background-color: #2f313680;
padding: 0.25rem 0.5rem; padding: 0.25rem 1.2rem;
border-radius: 0.25rem; border-radius: 1rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -32,21 +37,16 @@
.client { .client {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 0 0; gap: 0.5rem 0.5rem;
align-items: center; align-items: center;
margin: 0.5rem 0;
// icon styles &--align-right {
svg { flex-direction: row-reverse;
width: 5vw;
aspect-ratio: 1/1;
margin-right: 0.5rem;
filter: drop-shadow(0 0 0.75rem rgba(15, 15, 15, 0.1));
} }
// client name styles // client name styles
p { p {
font-size: 5vw; font-size: 4rem;
} }
} }
} }

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

@@ -1 +1,2 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

View File

@@ -1,6 +1,7 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import path from 'path'; import path from 'path';
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@@ -20,5 +21,5 @@ export default defineConfig({
outDir: 'dist', outDir: 'dist',
emptyOutDir: true, emptyOutDir: true,
}, },
plugins: [react()], plugins: [react(), svgr()],
}) })