mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Add Spring Boot Twitch overlay server with CI and Docker
This commit is contained in:
35
src/main/resources/application.yml
Normal file
35
src/main/resources/application.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8080}
|
||||
ssl:
|
||||
enabled: ${SSL_ENABLED:false}
|
||||
key-store: ${SSL_KEYSTORE_PATH:classpath:keystore.p12}
|
||||
key-store-password: ${SSL_KEYSTORE_PASSWORD:changeit}
|
||||
key-store-type: ${SSL_KEYSTORE_TYPE:PKCS12}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: imgfloat
|
||||
thymeleaf:
|
||||
cache: false
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
twitch:
|
||||
client-id: ${TWITCH_CLIENT_ID:changeme}
|
||||
client-secret: ${TWITCH_CLIENT_SECRET:changeme}
|
||||
redirect-uri: "{baseUrl}/login/oauth2/code/twitch"
|
||||
authorization-grant-type: authorization_code
|
||||
scope: ["user:read:email"]
|
||||
provider:
|
||||
twitch:
|
||||
authorization-uri: https://id.twitch.tv/oauth2/authorize
|
||||
token-uri: https://id.twitch.tv/oauth2/token
|
||||
user-info-uri: https://api.twitch.tv/helix/users
|
||||
user-name-attribute: preferred_username
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
84
src/main/resources/static/css/styles.css
Normal file
84
src/main/resources/static/css/styles.css
Normal file
@@ -0,0 +1,84 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 40px auto;
|
||||
background: #111827;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 5px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.button, button {
|
||||
background: #7c3aed;
|
||||
color: white;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.admin-layout header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 16px;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.controls ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.controls li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.overlay iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.overlay canvas, .broadcast-body canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.broadcast-body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
91
src/main/resources/static/js/admin.js
Normal file
91
src/main/resources/static/js/admin.js
Normal file
@@ -0,0 +1,91 @@
|
||||
let stompClient;
|
||||
const canvas = document.getElementById('admin-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
const images = new Map();
|
||||
|
||||
function connect() {
|
||||
const socket = new SockJS('/ws');
|
||||
stompClient = Stomp.over(socket);
|
||||
stompClient.connect({}, () => {
|
||||
stompClient.subscribe(`/topic/channel/${broadcaster}`, (payload) => {
|
||||
const body = JSON.parse(payload.body);
|
||||
handleEvent(body);
|
||||
});
|
||||
fetchImages();
|
||||
fetchAdmins();
|
||||
});
|
||||
}
|
||||
|
||||
function fetchImages() {
|
||||
fetch(`/api/channels/${broadcaster}/images`).then(r => r.json()).then(renderImages);
|
||||
}
|
||||
|
||||
function fetchAdmins() {
|
||||
fetch(`/api/channels/${broadcaster}/admins`).then(r => r.json()).then(list => {
|
||||
const adminList = document.getElementById('admin-list');
|
||||
adminList.innerHTML = '';
|
||||
list.forEach(a => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = a;
|
||||
adminList.appendChild(li);
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function renderImages(list) {
|
||||
list.forEach(img => images.set(img.id, img));
|
||||
draw();
|
||||
}
|
||||
|
||||
function handleEvent(event) {
|
||||
if (event.type === 'DELETED') {
|
||||
images.delete(event.imageId);
|
||||
} else if (event.payload) {
|
||||
images.set(event.payload.id, event.payload);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
images.forEach(img => {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = img.hidden ? 0.35 : 1;
|
||||
ctx.translate(img.x, img.y);
|
||||
ctx.rotate(img.rotation * Math.PI / 180);
|
||||
ctx.fillStyle = 'rgba(124, 58, 237, 0.25)';
|
||||
ctx.fillRect(0, 0, img.width, img.height);
|
||||
ctx.restore();
|
||||
});
|
||||
}
|
||||
|
||||
function uploadImage() {
|
||||
const url = document.getElementById('image-url').value;
|
||||
const width = parseFloat(document.getElementById('image-width').value);
|
||||
const height = parseFloat(document.getElementById('image-height').value);
|
||||
fetch(`/api/channels/${broadcaster}/images`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({url, width, height})
|
||||
});
|
||||
}
|
||||
|
||||
function addAdmin() {
|
||||
const usernameInput = document.getElementById('new-admin');
|
||||
const username = usernameInput.value;
|
||||
fetch(`/api/channels/${broadcaster}/admins`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({username})
|
||||
}).then(() => fetchAdmins());
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
draw();
|
||||
});
|
||||
|
||||
connect();
|
||||
57
src/main/resources/static/js/broadcast.js
Normal file
57
src/main/resources/static/js/broadcast.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const canvas = document.getElementById('broadcast-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
const images = new Map();
|
||||
|
||||
function connect() {
|
||||
const socket = new SockJS('/ws');
|
||||
const stompClient = Stomp.over(socket);
|
||||
stompClient.connect({}, () => {
|
||||
stompClient.subscribe(`/topic/channel/${broadcaster}`, (payload) => {
|
||||
const body = JSON.parse(payload.body);
|
||||
handleEvent(body);
|
||||
});
|
||||
fetch(`/api/channels/${broadcaster}/images/visible`).then(r => r.json()).then(renderImages);
|
||||
});
|
||||
}
|
||||
|
||||
function renderImages(list) {
|
||||
list.forEach(img => images.set(img.id, img));
|
||||
draw();
|
||||
}
|
||||
|
||||
function handleEvent(event) {
|
||||
if (event.type === 'DELETED') {
|
||||
images.delete(event.imageId);
|
||||
} else if (event.payload && !event.payload.hidden) {
|
||||
images.set(event.payload.id, event.payload);
|
||||
} else if (event.payload && event.payload.hidden) {
|
||||
images.delete(event.payload.id);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
images.forEach(img => {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.translate(img.x, img.y);
|
||||
ctx.rotate(img.rotation * Math.PI / 180);
|
||||
const image = new Image();
|
||||
image.src = img.url;
|
||||
image.onload = () => {
|
||||
ctx.drawImage(image, 0, 0, img.width, img.height);
|
||||
};
|
||||
ctx.restore();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
draw();
|
||||
});
|
||||
|
||||
connect();
|
||||
48
src/main/resources/templates/admin.html
Normal file
48
src/main/resources/templates/admin.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Imgfloat Admin</title>
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-layout">
|
||||
<header>
|
||||
<h2>Channel <span th:text="${broadcaster}"></span> overlay controls</h2>
|
||||
<div class="actions">
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button class="secondary" type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
<section class="controls">
|
||||
<div>
|
||||
<h3>Admins</h3>
|
||||
<input id="new-admin" placeholder="Twitch username" />
|
||||
<button onclick="addAdmin()">Add admin</button>
|
||||
<ul id="admin-list"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Images</h3>
|
||||
<input id="image-url" placeholder="Image URL" />
|
||||
<input id="image-width" placeholder="Width" type="number" value="600" />
|
||||
<input id="image-height" placeholder="Height" type="number" value="400" />
|
||||
<button onclick="uploadImage()">Upload</button>
|
||||
<ul id="image-list"></ul>
|
||||
</div>
|
||||
</section>
|
||||
<section class="overlay">
|
||||
<iframe th:src="${'https://player.twitch.tv/?channel=' + broadcaster + '&parent=localhost'}" allowfullscreen></iframe>
|
||||
<canvas id="admin-canvas"></canvas>
|
||||
</section>
|
||||
</div>
|
||||
<script th:inline="javascript">
|
||||
const broadcaster = /*[[${broadcaster}]]*/ '';
|
||||
const username = /*[[${username}]]*/ '';
|
||||
</script>
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
17
src/main/resources/templates/broadcast.html
Normal file
17
src/main/resources/templates/broadcast.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Imgfloat Broadcast</title>
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||
</head>
|
||||
<body class="broadcast-body">
|
||||
<canvas id="broadcast-canvas"></canvas>
|
||||
<script th:inline="javascript">
|
||||
const broadcaster = /*[[${broadcaster}]]*/ '';
|
||||
</script>
|
||||
<script src="/js/broadcast.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
src/main/resources/templates/dashboard.html
Normal file
21
src/main/resources/templates/dashboard.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Imgfloat Dashboard</title>
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome, <span th:text="${username}">user</span></h1>
|
||||
<p>Manage your overlay or invite channel admins.</p>
|
||||
<div class="actions">
|
||||
<a class="button" th:href="@{'/view/' + ${channel} + '/admin'}">Admin console</a>
|
||||
<a class="button" th:href="@{'/view/' + ${channel} + '/broadcast'}">Broadcast overlay</a>
|
||||
<form th:action="@{/logout}" method="post">
|
||||
<button class="secondary" type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
src/main/resources/templates/index.html
Normal file
15
src/main/resources/templates/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Imgfloat - Twitch overlay</title>
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Imgfloat</h1>
|
||||
<p>Authenticate with Twitch to manage your channel overlays and invite channel admins.</p>
|
||||
<a class="button" href="/oauth2/authorization/twitch">Login with Twitch</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user