Zirnavis / index.html
Opera8's picture
Update index.html
d9d8067 verified
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>AI Subtitle Monster</title>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;500;700;900&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--bg-dark: #0f172a;
--bg-card: #1e293b;
--primary: #8b5cf6;
--primary-glow: rgba(139, 92, 246, 0.5);
--accent: #f43f5e;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--border: #334155;
--success: #10b981;
}
* { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
body {
font-family: 'Vazirmatn', sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
margin: 0;
padding: 0;
overflow-x: hidden;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Animated BG */
.bg-mesh {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1;
background: radial-gradient(circle at 50% 50%, #1e1b4b 0%, #0f172a 100%);
overflow: hidden;
}
.blob {
position: absolute;
filter: blur(80px);
opacity: 0.4;
animation: float 10s infinite ease-in-out;
}
.blob-1 { top: -10%; left: -10%; width: 50vw; height: 50vw; background: var(--primary); }
.blob-2 { bottom: -10%; right: -10%; width: 40vw; height: 40vw; background: var(--accent); animation-delay: -5s; }
@keyframes float { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(30px, 50px); } }
.app-header {
padding: 15px 20px;
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
position: sticky;
top: 0;
}
.brand { font-weight: 900; font-size: 1.4rem; background: linear-gradient(to right, var(--primary), var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
.app-body {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
max-width: 800px;
margin: 0 auto;
width: 100%;
}
/* Views */
.view { display: none; animation: fadeIn 0.4s ease; }
.view.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3);
}
.btn {
width: 100%;
padding: 16px;
border-radius: 14px;
border: none;
font-weight: 800;
font-size: 1.1rem;
cursor: pointer;
transition: 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
color: white;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), #4338ca);
box-shadow: 0 0 20px var(--primary-glow);
}
.btn-primary:active { transform: scale(0.98); }
/* Uploader */
.upload-zone {
border: 2px dashed var(--border);
border-radius: 20px;
height: 300px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transition: 0.3s;
background: rgba(255,255,255,0.02);
}
.upload-zone:hover { border-color: var(--primary); background: rgba(99, 102, 241, 0.05); }
.upload-icon { font-size: 4rem; margin-bottom: 20px; color: var(--text-muted); }
/* Editor & Settings */
.segment-container {
max-height: 400px;
overflow-y: auto;
padding-right: 5px;
margin-bottom: 25px;
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px;
background: rgba(0,0,0,0.2);
}
.segment-row {
background: rgba(255,255,255,0.03);
border-radius: 12px;
padding: 10px;
margin-bottom: 10px;
border-right: 3px solid transparent;
}
.segment-row:focus-within { border-right-color: var(--accent); background: rgba(255,255,255,0.06); }
.seg-time { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 5px; font-family: monospace; }
.seg-input {
width: 100%;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: 8px;
color: #fff;
font-size: 1.1rem;
font-family: inherit;
resize: vertical;
padding: 10px;
min-height: 60px;
line-height: 1.5;
}
.seg-input:focus { border-color: var(--primary); background: rgba(0, 0, 0, 0.5); }
.control-group { margin-bottom: 18px; }
.control-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
input[type="range"] { width: 100%; height: 6px; background: var(--border); border-radius: 5px; appearance: none; }
input[type="range"]::-webkit-slider-thumb { appearance: none; width: 20px; height: 20px; background: var(--primary); border-radius: 50%; cursor: pointer; }
input[type="color"] { width: 100%; height: 40px; border: none; border-radius: 8px; cursor: pointer; background: transparent; }
.style-chips { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px; }
.chip {
padding: 8px 16px; background: var(--border); border-radius: 20px;
font-size: 0.85rem; cursor: pointer; white-space: nowrap; transition: 0.2s;
}
.chip.active { background: rgba(99, 102, 241, 0.2); color: var(--primary); border: 1px solid var(--primary); }
.preview-box {
position: relative; width: 100%; aspect-ratio: 16/9;
background: #000; border-radius: 12px; margin-bottom: 20px;
display: flex; align-items: center; justify-content: center; overflow: hidden;
background-image: url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1000&auto=format&fit=crop');
background-size: cover;
}
.preview-text {
position: absolute; text-align: center; pointer-events: none;
transition: 0.1s; line-height: 1.4; max-width: 80%;
}
#inlineResult {
margin-top: 30px;
border-top: 2px dashed var(--border);
padding-top: 20px;
text-align: center;
display: none;
animation: slideDown 0.5s ease;
}
@keyframes slideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
.result-video { width: 100%; border-radius: 12px; margin-top: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.dl-btn {
display: inline-block; margin-top: 15px; padding: 15px 30px;
background: var(--success); color: white; text-decoration: none;
border-radius: 12px; font-weight: bold; font-size: 1.1rem;
box-shadow: 0 5px 20px rgba(16, 185, 129, 0.3);
}
/* Loader */
.loader-screen {
position: fixed; top:0; left:0; width:100%; height:100%;
background: rgba(15, 23, 42, 0.95); z-index: 1000;
display: none; flex-direction: column; justify-content: center; align-items: center;
}
.loader-screen.flex { display: flex; }
.spinner { width: 50px; height: 50px; border: 5px solid var(--border); border-top: 5px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; }
@keyframes spin { 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="bg-mesh"><div class="blob blob-1"></div><div class="blob blob-2"></div></div>
<div id="loader" class="loader-screen">
<div class="spinner"></div>
<h3 id="loaderMsg">در حال پردازش...</h3>
</div>
<header class="app-header">
<div class="brand"><i class="fa-solid fa-wand-magic-sparkles"></i> زیرنویس (آلفا)</div>
</header>
<div class="app-body">
<!-- VIEW 1: UPLOAD -->
<div id="view-upload" class="view active">
<div style="max-width: 600px; margin: 40px auto;">
<div class="card">
<h2 style="text-align: center; color: var(--text-main);">آپلود ویدیو</h2>
<div class="upload-zone" onclick="document.getElementById('fileIn').click()">
<i class="fa-solid fa-cloud-arrow-up upload-icon"></i>
<h3>انتخاب فایل</h3>
<input type="file" id="fileIn" hidden accept="video/*" onchange="handleUpload()">
</div>
</div>
</div>
</div>
<!-- VIEW 2: MAIN EDITOR -->
<div id="view-editor" class="view">
<div class="card">
<!-- 1. SETTINGS & PREVIEW -->
<h2 style="color: var(--primary); margin-top:0;">🎨 تنظیمات ظاهری</h2>
<div class="preview-box">
<div id="livePreview" class="preview-text">پیش‌نمایش متن</div>
</div>
<div class="control-group">
<div class="control-label"><span>فونت</span></div>
<div class="style-chips">
<div class="chip active" onclick="setFont('lalezar', this)">لاله زار (فا)</div>
<div class="chip" onclick="setFont('vazir', this)">وزیر (فا)</div>
<div class="chip" onclick="setFont('roboto', this)">Roboto (Eng)</div>
<div class="chip" onclick="setFont('bangers', this)">Bangers (Eng)</div>
</div>
</div>
<div class="control-group">
<div class="control-label"><span>رنگ متن</span></div>
<input type="color" id="colorMain" value="#FFFFFF" oninput="updatePreview()">
</div>
<div class="control-group">
<div class="control-label"><span>رنگ کادر</span></div>
<input type="color" id="colorOutline" value="#000000" oninput="updatePreview()">
</div>
<div class="control-group">
<div class="control-label"><span>نوع کادر</span></div>
<div class="style-chips">
<div class="chip active" onclick="setStyle('solid', this)">پُر رنگ</div>
<div class="chip" onclick="setStyle('transparent', this)">شیشه‌ای</div>
<div class="chip" onclick="setStyle('outline', this)">حاشیه</div>
</div>
</div>
<div class="control-group">
<div class="control-label"><span>سایز متن</span> <span id="lblSize">100</span></div>
<input type="range" id="rngSize" min="30" max="400" value="100" oninput="updatePreview()">
</div>
<div class="control-group">
<div class="control-label"><span>موقعیت عمودی</span></div>
<input type="range" id="rngPos" min="10" max="600" value="150" oninput="updatePreview()">
</div>
<hr style="border-color: var(--border); margin: 30px 0;">
<!-- 2. EDITOR LIST -->
<h2 style="color: var(--text-main);">📝 ویرایش متن‌ها</h2>
<div id="segmentsList" class="segment-container"></div>
<!-- 3. ACTION BUTTON -->
<button class="btn btn-primary" onclick="startRender()">
<i class="fa-solid fa-bolt"></i> ساخت ویدیو نهایی
</button>
<!-- 4. RESULT AREA -->
<div id="inlineResult">
<h3 style="color: var(--success); margin:0;">✅ ویدیو آماده شد!</h3>
<video id="finalPlayer" controls class="result-video"></video>
<a id="dlBtn" href="#" download class="dl-btn">دانلود ویدیو</a>
</div>
</div>
</div>
</div>
<script>
let appState = {
fileId: null,
segments: [],
style: {
backType: 'solid',
font: 'lalezar'
}
};
// Storage Keys
const STORAGE_KEYS = ['colorMain', 'colorOutline', 'rngSize', 'rngPos'];
// Load Settings
document.addEventListener('DOMContentLoaded', () => {
STORAGE_KEYS.forEach(key => {
const val = localStorage.getItem(key);
if(val) {
document.getElementById(key).value = val;
if(key === 'rngSize') document.getElementById('lblSize').innerText = val;
}
});
});
// Save Settings
STORAGE_KEYS.forEach(key => {
document.getElementById(key).addEventListener('input', (e) => {
localStorage.setItem(key, e.target.value);
});
});
// --- UPLOAD ---
async function handleUpload() {
const file = document.getElementById('fileIn').files[0];
if(!file) return;
showLoader("در حال استخراج متن...");
const formData = new FormData();
formData.append("file", file);
try {
const res = await fetch("/api/analyze", { method: "POST", body: formData });
const data = await res.json();
if(data.error) throw new Error(data.error);
appState.fileId = data.file_id;
appState.segments = data.segments;
renderSegments();
document.getElementById('view-upload').classList.remove('active');
document.getElementById('view-editor').classList.add('active');
updatePreview();
window.scrollTo(0, 0);
} catch(e) {
alert("Error: " + e.message);
} finally {
hideLoader();
}
}
// --- EDITOR ---
function renderSegments() {
const container = document.getElementById('segmentsList');
container.innerHTML = "";
appState.segments.forEach((seg, idx) => {
const div = document.createElement('div');
div.className = 'segment-row';
div.innerHTML = `
<div class="seg-time">${formatTime(seg.start)} -> ${formatTime(seg.end)}</div>
<textarea class="seg-input" rows="1" oninput="updateSegment(${idx}, this)">${seg.text}</textarea>
`;
container.appendChild(div);
});
}
function updateSegment(idx, el) {
appState.segments[idx].text = el.value;
document.getElementById('livePreview').innerText = el.value;
el.style.height = 'auto';
el.style.height = (el.scrollHeight) + 'px';
}
function formatTime(s) {
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return `${m}:${sec.toString().padStart(2, '0')}`;
}
// --- PREVIEW ---
function setStyle(type, el) {
appState.style.backType = type;
el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
el.classList.add('active');
updatePreview();
}
function setFont(font, el) {
appState.style.font = font;
el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
el.classList.add('active');
updatePreview();
}
function updatePreview() {
const txt = document.getElementById('livePreview');
const size = document.getElementById('rngSize').value;
const pos = document.getElementById('rngPos').value;
const color = document.getElementById('colorMain').value;
const outline = document.getElementById('colorOutline').value;
let font = 'Vazirmatn';
if(appState.style.font === 'lalezar') font = 'Lalezar';
if(appState.style.font === 'roboto') font = 'Arial';
if(appState.style.font === 'bangers') font = 'Impact';
document.getElementById('lblSize').innerText = size;
txt.style.fontFamily = font;
txt.style.fontSize = (size / 5) + 'px';
txt.style.color = color;
txt.style.bottom = (pos / 6) + 'px';
if(appState.style.backType === 'solid') {
txt.style.backgroundColor = outline;
txt.style.textShadow = 'none';
txt.style.padding = '2px 8px';
txt.style.borderRadius = '4px';
txt.style.webkitTextStroke = '0px';
} else if (appState.style.backType === 'transparent') {
txt.style.backgroundColor = 'rgba(0,0,0,0.6)';
txt.style.textShadow = 'none';
txt.style.padding = '2px 8px';
txt.style.borderRadius = '4px';
txt.style.webkitTextStroke = '0px';
} else {
txt.style.backgroundColor = 'transparent';
txt.style.webkitTextStroke = `1px ${outline}`;
txt.style.textShadow = `0 0 2px ${outline}`;
txt.style.padding = '0';
}
}
// --- RENDER ---
async function startRender() {
document.getElementById('inlineResult').style.display = 'none';
showLoader("در حال ساخت ویدیو...");
const payload = {
file_id: appState.fileId,
segments: appState.segments,
style: {
font: appState.style.font,
fontSize: parseInt(document.getElementById('rngSize').value),
primaryColor: document.getElementById('colorMain').value,
outlineColor: document.getElementById('colorOutline').value,
backType: appState.style.backType,
outlineWidth: 2.0,
marginV: parseInt(document.getElementById('rngPos').value),
alignment: 2
}
};
try {
const res = await fetch("/api/render", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if(data.error) throw new Error(data.error);
const resultBox = document.getElementById('inlineResult');
resultBox.style.display = 'block';
document.getElementById('finalPlayer').src = data.url + "?t=" + Date.now();
document.getElementById('dlBtn').href = data.url;
resultBox.scrollIntoView({behavior: 'smooth'});
} catch(e) {
alert("Render Error: " + e.message);
} finally {
hideLoader();
}
}
function showLoader(msg) {
document.getElementById('loaderMsg').innerText = msg;
document.getElementById('loader').classList.add('flex');
}
function hideLoader() {
document.getElementById('loader').classList.remove('flex');
}
</script>
</body>
</html>