2026-07-01 15:53:07 +00:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="ru">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
|
<title>Send2Streamer — сообщения от зрителей прямо на стрим</title>
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
2026-07-08 18:21:57 +00:00
|
|
|
|
<script>
|
|
|
|
|
|
window.googleGsiLoaded = new Promise((resolve) => { window.resolveGoogleGsiLoaded = resolve; });
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<script src="https://accounts.google.com/gsi/client" async defer onload="resolveGoogleGsiLoaded()"></script>
|
2026-07-01 15:53:07 +00:00
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: system-ui, sans-serif;
|
|
|
|
|
|
max-width: 640px;
|
|
|
|
|
|
margin: 60px auto;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
h1 {
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.steps {
|
|
|
|
|
|
background: #f6f6f6;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.steps li {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
#login-container {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
#status {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
|
|
<h1>Send2Streamer</h1>
|
|
|
|
|
|
<p class="subtitle">Зрители пишут сообщения — они всплывают у вас на стриме голосом.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="steps">
|
|
|
|
|
|
<p><strong>Как это работает:</strong></p>
|
|
|
|
|
|
<ol>
|
|
|
|
|
|
<li>Вы входите через Google — сервис выдаёт вам две ссылки: для OBS/StreamLabs и для зрителей.</li>
|
|
|
|
|
|
<li>Первую вставляете в Browser Source в OBS/StreamLabs.</li>
|
|
|
|
|
|
<li>Вторую публикуете для зрителей (например, в описании канала или в чате).</li>
|
|
|
|
|
|
<li>Зритель авторизуется через Google по этой ссылке и пишет вам сообщение — оно появляется на стриме с текстом и озвучкой.</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p><strong>Войдите как стример, чтобы получить свои ссылки:</strong></p>
|
|
|
|
|
|
<div id="login-container"></div>
|
|
|
|
|
|
<div id="status"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
async function handleCredentialResponse(response) {
|
|
|
|
|
|
const statusEl = document.getElementById('status');
|
|
|
|
|
|
const res = await fetch('/auth/google', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ credential: response.credential }),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
window.location.href = '/dashboard.html';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
statusEl.textContent = 'Не удалось войти, попробуйте ещё раз';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function init() {
|
|
|
|
|
|
const meRes = await fetch('/me');
|
|
|
|
|
|
const { user } = await meRes.json();
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
window.location.href = '/dashboard.html';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const configRes = await fetch('/config');
|
|
|
|
|
|
const { googleClientId } = await configRes.json();
|
|
|
|
|
|
|
2026-07-08 18:21:57 +00:00
|
|
|
|
// The GSI script tag is async/defer — without waiting for it to actually
|
|
|
|
|
|
// finish loading, this can run before `google` exists (a race that fails
|
|
|
|
|
|
// silently), which is why the button sometimes only appeared after a
|
|
|
|
|
|
// reload (once the script was already cached).
|
|
|
|
|
|
await window.googleGsiLoaded;
|
|
|
|
|
|
|
2026-07-01 15:53:07 +00:00
|
|
|
|
google.accounts.id.initialize({
|
|
|
|
|
|
client_id: googleClientId,
|
|
|
|
|
|
callback: handleCredentialResponse,
|
|
|
|
|
|
});
|
|
|
|
|
|
google.accounts.id.renderButton(document.getElementById('login-container'), { theme: 'outline', size: 'large' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|