Files
WFL/templates/login.html

565 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Вход в управлялку vMix под ЖФЛ</title>
<link rel="icon" href="/static/smith.ico" type="image/x-icon">
<style>
:root {
--bg: #000000;
--green: #00ff66;
--green-soft: #00cc55;
--green-dim: rgba(0, 255, 102, 0.7);
--panel: rgba(0, 20, 0, 0.22);
--border: rgba(0, 255, 102, 0.22);
--danger: #ff5f56;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--bg);
color: var(--green);
font-family: "Courier New", monospace;
}
body {
position: relative;
}
#matrixCanvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
opacity: 0.22;
}
.screen {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.terminal {
width: min(900px, 100%);
min-height: 420px;
padding: 32px;
border: 1px solid var(--border);
background: linear-gradient(
180deg,
rgba(0, 18, 0, 0.35) 0%,
rgba(0, 0, 0, 0.72) 100%
);
box-shadow:
0 0 24px rgba(0, 255, 102, 0.08),
inset 0 0 30px rgba(0, 255, 102, 0.04);
backdrop-filter: blur(2px);
}
.terminal-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--green);
box-shadow: 0 0 8px rgba(0, 255, 102, 0.5);
}
.terminal-title {
margin-left: 8px;
font-size: 13px;
color: var(--green-dim);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.terminal-body {
font-size: 28px;
line-height: 1.8;
text-shadow: 0 0 10px rgba(0, 255, 102, 0.18);
}
.term-line {
display: block;
min-height: 1.8em;
}
.prompt-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.prompt {
color: var(--green);
}
.input-wrap {
position: relative;
display: inline-flex;
align-items: center;
min-width: 220px;
}
.fake-input {
position: absolute;
left: -9999px;
opacity: 0;
pointer-events: none;
}
.typed-value {
color: var(--green);
}
.caret {
display: inline;
margin-left: -1px; /* чуть прижать к тексту */
}
.hidden {
display: none;
}
.sys-message {
margin-top: 18px;
font-size: 16px;
color: var(--green-dim);
line-height: 1.5;
}
.sys-error {
color: var(--danger);
text-shadow: 0 0 8px rgba(255, 95, 86, 0.25);
}
.subtle {
opacity: 0.8;
}
.scanline::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.02),
rgba(255, 255, 255, 0.00) 3px
);
background-size: 100% 4px;
opacity: 0.08;
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
@media (max-width: 700px) {
.terminal {
padding: 22px;
min-height: 360px;
}
.terminal-body {
font-size: 20px;
line-height: 1;
}
.sys-message {
font-size: 14px;
}
}
</style>
</head>
<body class="scanline">
<canvas id="matrixCanvas"></canvas>
<div class="screen">
<div class="terminal">
<div class="terminal-header">
<span class="dot"></span>
<span class="dot" style="opacity: 0.7;"></span>
<span class="dot" style="opacity: 0.45;"></span>
<div class="terminal-title">wfl access terminal</div>
</div>
<div class="terminal-body" id="terminalBody">
<div class="term-line" id="welcomeLine"></div>
<div class="term-line hidden" id="loginLine">
<span id="loginPrompt" class="prompt"></span>
<span id="usernameText" class="typed-value"></span><span id="usernameCaret" class="caret"></span>
<input id="usernameInput" class="fake-input" type="text" autocomplete="username" spellcheck="false">
</div>
<div class="term-line hidden" id="passwordLine">
<span id="passwordPrompt" class="prompt"></span>
<span id="passwordText" class="typed-value"></span><span id="passwordCaret" class="caret hidden"></span>
<input id="passwordInput" class="fake-input" type="password" autocomplete="current-password" spellcheck="false">
</div>
<div class="term-line hidden subtle" id="submitLine">
authorizing...
</div>
{% if message %}
<div class="sys-message">{{ message }}</div>
{% endif %}
{% if error %}
<div class="sys-message sys-error">{{ error }}</div>
{% endif %}
</div>
</div>
</div>
<form id="realLoginForm" method="post" action="/login" class="hidden">
<input type="hidden" name="username" id="realUsername">
<input type="hidden" name="password" id="realPassword">
</form>
<script>
(function () {
const canvas = document.getElementById("matrixCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
let matrixAnimationId = null;
let drops = [];
const fontSize = 18;
const chars = "アイウエオカキクケコサシスセソ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZЖФЛ<>+*#";
let columns = 0;
let lastTime = 0;
const fps = 18;
const interval = 1000 / fps;
function resizeCanvas() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.floor(canvas.width / fontSize);
drops = Array(columns).fill(1);
}
function drawMatrix(time = 0) {
if (!canvas || !ctx) return;
if (time - lastTime < interval) {
matrixAnimationId = requestAnimationFrame(drawMatrix);
return;
}
lastTime = time;
ctx.fillStyle = "rgba(0, 0, 0, 0.12)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00ff66";
ctx.font = fontSize + "px monospace";
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height && Math.random() > 0.985) {
drops[i] = 0;
}
drops[i] += 0.65;
}
matrixAnimationId = requestAnimationFrame(drawMatrix);
}
resizeCanvas();
drawMatrix();
window.addEventListener("resize", resizeCanvas);
const welcomeText = "добро пожаловать в ЖФЛ";
const welcomeLine = document.getElementById("welcomeLine");
const loginLine = document.getElementById("loginLine");
const passwordLine = document.getElementById("passwordLine");
const submitLine = document.getElementById("submitLine");
const loginPrompt = document.getElementById("loginPrompt");
const passwordPrompt = document.getElementById("passwordPrompt");
const usernameInput = document.getElementById("usernameInput");
const passwordInput = document.getElementById("passwordInput");
const usernameText = document.getElementById("usernameText");
const passwordText = document.getElementById("passwordText");
const usernameCaret = document.getElementById("usernameCaret");
const passwordCaret = document.getElementById("passwordCaret");
const realLoginForm = document.getElementById("realLoginForm");
const realUsername = document.getElementById("realUsername");
const realPassword = document.getElementById("realPassword");
const hasServerError = !!document.querySelector(".sys-error");
let phase = "waiting_start";
let started = false;
let audioCtx = null;
function ensureAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === "suspended") {
audioCtx.resume();
}
return audioCtx;
}
function playKeySound() {
const ctx = ensureAudioContext();
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.type = "square";
oscillator.frequency.setValueAtTime(900, ctx.currentTime);
gainNode.gain.setValueAtTime(0.012, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.035);
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.start();
oscillator.stop(ctx.currentTime + 0.035);
}
function typeText(target, text, speed = 60, callback, withSound = true) {
let i = 0;
function typing() {
if (i < text.length) {
target.textContent += text[i];
if (withSound) playKeySound();
i++;
setTimeout(typing, speed);
} else if (callback) {
callback();
}
}
typing();
}
function focusCurrentInput() {
if (phase === "login") {
usernameInput.focus();
} else if (phase === "password") {
passwordInput.focus();
}
}
function showLoginStep(prefill = "") {
phase = "typing_login";
loginLine.classList.remove("hidden");
usernameCaret.classList.add("hidden");
passwordCaret.classList.add("hidden");
loginPrompt.textContent = "";
usernameText.textContent = "";
usernameInput.value = "";
typeText(loginPrompt, "login:", 60, () => {
if (prefill) {
usernameInput.value = prefill;
usernameText.textContent = prefill;
}
phase = "login";
usernameCaret.classList.remove("hidden");
usernameInput.focus();
});
}
function showPasswordStep(prefill = "") {
phase = "typing_password";
passwordLine.classList.remove("hidden");
usernameCaret.classList.add("hidden");
passwordCaret.classList.add("hidden");
passwordPrompt.textContent = "";
passwordText.textContent = "";
passwordInput.value = "";
typeText(passwordPrompt, "password:", 60, () => {
if (prefill) {
passwordInput.value = prefill;
passwordText.textContent = "*".repeat(prefill.length);
}
phase = "password";
passwordCaret.classList.remove("hidden");
passwordInput.focus();
});
}
function submitLogin() {
phase = "submitting";
usernameCaret.classList.add("hidden");
passwordCaret.classList.add("hidden");
submitLine.classList.remove("hidden");
realUsername.value = usernameInput.value.trim();
realPassword.value = passwordInput.value;
setTimeout(() => {
realLoginForm.submit();
}, 350);
}
function startSequence(options = {}) {
if (started) return;
started = true;
const skipWelcome = !!options.skipWelcome;
const usernamePrefill = options.usernamePrefill || "";
ensureAudioContext();
phase = "typing_welcome";
welcomeLine.textContent = "";
loginPrompt.textContent = "";
passwordPrompt.textContent = "";
usernameText.textContent = "";
passwordText.textContent = "";
usernameInput.value = "";
passwordInput.value = "";
submitLine.classList.add("hidden");
loginLine.classList.add("hidden");
passwordLine.classList.add("hidden");
usernameCaret.classList.add("hidden");
passwordCaret.classList.add("hidden");
if (skipWelcome) {
welcomeLine.textContent = welcomeText;
setTimeout(() => showLoginStep(usernamePrefill), 120);
return;
}
typeText(welcomeLine, welcomeText, 75, () => {
setTimeout(() => showLoginStep(usernamePrefill), 220);
});
}
usernameInput.addEventListener("input", function () {
usernameText.textContent = usernameInput.value;
playKeySound();
});
passwordInput.addEventListener("input", function () {
passwordText.textContent = "*".repeat(passwordInput.value.length);
playKeySound();
});
usernameInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
playKeySound();
event.preventDefault();
if (!usernameInput.value.trim()) {
return;
}
usernameCaret.classList.add("hidden");
showPasswordStep();
}
});
passwordInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
playKeySound();
event.preventDefault();
if (!usernameInput.value.trim() || !passwordInput.value) {
return;
}
submitLogin();
}
});
document.addEventListener("click", function () {
if (!started) {
startSequence();
return;
}
focusCurrentInput();
});
document.addEventListener("keydown", function (event) {
if (!started) {
event.preventDefault();
startSequence();
return;
}
if (
phase === "typing_welcome" ||
phase === "typing_login" ||
phase === "typing_password" ||
phase === "submitting"
) {
return;
}
if (document.activeElement === usernameInput || document.activeElement === passwordInput) {
return;
}
if (event.key === "Tab") {
event.preventDefault();
focusCurrentInput();
return;
}
if (event.key.length === 1 || event.key === "Backspace") {
focusCurrentInput();
}
});
if (hasServerError) {
startSequence({ skipWelcome: true });
}
})();
</script>
</body>
</html>