Translate / index.html
Opera10's picture
Update index.html
1122a41 verified
Raw
History Blame Contribute Delete
37.1 kB
<!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>آلفا مترجم | Alpha Translator</title>
<link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.0/Vazirmatn-font-face.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--sky: #38bdf8;
--sky-deep: #0ea5e9;
--sky-pale: #e0f2fe;
--ocean: #0369a1;
--mint: #34d399;
--lavender: #818cf8;
--peach: #fb923c;
--bg: #f0f9ff;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--border: #e2e8f0;
--shadow: 0 20px 60px -10px rgba(14,165,233,0.15);
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
body {
font-family: 'Vazirmatn', sans-serif;
background: var(--bg);
min-height: 100vh;
overflow-x: hidden;
color: var(--text);
}
/* ===== BACKGROUND ===== */
.bg-scene {
position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none;
}
.bg-scene::before {
content: ''; position: absolute; top: -20%; right: -10%;
width: 70vw; height: 70vw; border-radius: 50%;
background: radial-gradient(circle, rgba(56,189,248,0.18) 0%, transparent 70%);
animation: floatBlob 12s ease-in-out infinite;
}
.bg-scene::after {
content: ''; position: absolute; bottom: -10%; left: -5%;
width: 55vw; height: 55vw; border-radius: 50%;
background: radial-gradient(circle, rgba(129,140,248,0.14) 0%, transparent 70%);
animation: floatBlob 15s ease-in-out infinite reverse;
}
@keyframes floatBlob {
0%,100% { transform: translate(0,0) scale(1); }
33% { transform: translate(3%,4%) scale(1.04); }
66% { transform: translate(-2%,2%) scale(0.97); }
}
/* ===== LAYOUT ===== */
.app-wrapper { position: relative; z-index: 1; max-width: 720px; margin: 0 auto; padding: 0 16px 40px; }
/* ===== HEADER ===== */
header {
padding: 32px 0 20px; text-align: center;
animation: headerDrop 0.8s cubic-bezier(0.34,1.56,0.64,1) both;
}
@keyframes headerDrop {
0% { opacity:0; transform: translateY(-40px) scale(0.9); }
100% { opacity:1; transform: translateY(0) scale(1); }
}
.logo-wrap { display: inline-flex; align-items: center; justify-content: center; gap: 14px; }
/* ===== LOGO ICON ===== */
.logo-icon-wrap { position: relative; width: 64px; height: 64px; }
.logo-icon-wrap svg { width: 64px; height: 64px; filter: drop-shadow(0 4px 18px rgba(14,165,233,0.4)); }
.logo-icon-pulse {
position: absolute; inset: -6px; border-radius: 50%;
border: 2px solid rgba(56,189,248,0.5);
animation: pulse-ring 2s ease-out infinite;
}
.logo-icon-pulse2 {
position: absolute; inset: -14px; border-radius: 50%;
border: 2px solid rgba(56,189,248,0.25);
animation: pulse-ring 2s ease-out 0.6s infinite;
}
@keyframes pulse-ring {
0% { transform: scale(0.9); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
.logo-text { display: flex; flex-direction: column; align-items: flex-start; line-height: 1; gap: 4px; }
.logo-title {
font-size: 2rem; font-weight: 900;
background: linear-gradient(135deg, var(--sky-deep) 0%, var(--lavender) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.logo-sub { font-size: 0.72rem; color: var(--muted); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; }
/* ===== CARD ===== */
.card {
background: rgba(255,255,255,0.82);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255,255,255,0.9);
border-radius: 28px; padding: 28px 24px;
box-shadow: var(--shadow), 0 1px 0 rgba(255,255,255,0.9) inset;
animation: cardRise 0.7s cubic-bezier(0.34,1.2,0.64,1) 0.2s both;
}
@keyframes cardRise {
0% { opacity:0; transform: translateY(30px) scale(0.97); }
100% { opacity:1; transform: translateY(0) scale(1); }
}
/* ===== LANGUAGE ROW ===== */
.lang-row { display: flex; align-items: flex-end; gap: 10px; margin-bottom: 20px; }
.lang-group { flex: 1; }
.lang-label {
display: flex; align-items: center; gap: 6px;
font-size: 0.7rem; font-weight: 700; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px;
}
.lang-label .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--sky-deep); }
.select-wrap { position: relative; }
.select-wrap select {
width: 100%; appearance: none; -webkit-appearance: none;
background: rgba(248,250,252,0.9);
border: 1.5px solid var(--border); border-radius: 14px;
padding: 13px 14px 13px 40px;
font-family: 'Vazirmatn', sans-serif; font-size: 0.9rem; font-weight: 600;
color: var(--text); cursor: pointer; transition: all 0.25s ease; outline: none;
}
.select-wrap select:focus {
border-color: var(--sky); background: #fff;
box-shadow: 0 0 0 4px rgba(56,189,248,0.12);
}
.select-arrow {
position: absolute; left: 14px; top: 50%;
transform: translateY(-50%); color: var(--muted);
pointer-events: none; font-size: 0.7rem;
}
.swap-btn {
flex-shrink: 0; width: 44px; height: 44px; border-radius: 50%;
background: linear-gradient(135deg, var(--sky-pale), #ede9fe);
border: 1.5px solid rgba(56,189,248,0.3);
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.35s cubic-bezier(0.34,1.56,0.64,1);
color: var(--sky-deep); font-size: 0.85rem; margin-bottom: 2px;
}
.swap-btn:hover {
transform: rotate(180deg) scale(1.12);
background: linear-gradient(135deg, #bae6fd, #c7d2fe);
box-shadow: 0 4px 16px rgba(56,189,248,0.3);
}
/* ===== TEXTAREA ===== */
.textarea-wrap { position: relative; margin-bottom: 28px; }
.textarea-wrap textarea {
width: 100%; min-height: 140px; resize: none;
border: 1.5px solid var(--border); border-radius: 18px;
padding: 18px 18px 50px;
font-family: 'Vazirmatn', sans-serif; font-size: 1.05rem; line-height: 1.8;
color: var(--text); background: rgba(248,250,252,0.8);
outline: none; transition: all 0.3s ease;
}
.textarea-wrap textarea:focus {
border-color: var(--sky); background: #fff;
box-shadow: 0 0 0 4px rgba(56,189,248,0.1);
}
.textarea-wrap textarea::placeholder { color: #94a3b8; }
.textarea-wrap textarea.shake {
animation: shakeField 0.5s cubic-bezier(0.36,0.07,0.19,0.97) both;
border-color: #fb923c !important;
box-shadow: 0 0 0 4px rgba(251,146,60,0.15) !important;
}
@keyframes shakeField {
10%,90% { transform: translateX(-4px); }
20%,80% { transform: translateX(6px); }
30%,50%,70% { transform: translateX(-6px); }
40%,60% { transform: translateX(6px); }
}
.textarea-hint {
position: absolute; bottom: -26px; right: 0;
font-size: 0.78rem; color: #fb923c; font-weight: 700;
opacity: 0; transform: translateY(-4px); transition: all 0.3s ease;
display: flex; align-items: center; gap: 5px;
}
.textarea-hint.show { opacity: 1; transform: translateY(0); }
.clear-btn {
position: absolute; bottom: 14px; left: 14px;
width: 32px; height: 32px; border-radius: 50%;
background: white; border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
cursor: pointer; color: var(--muted); font-size: 0.8rem;
transition: all 0.2s; box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.clear-btn:hover { color: #ef4444; transform: scale(1.1); }
/* ===== TRANSLATE BUTTON ===== */
.translate-btn-wrap { margin-top: 4px; }
.translate-btn {
width: 100%; padding: 17px 24px; border-radius: 18px; border: none;
background: linear-gradient(135deg, var(--sky-deep) 0%, #6366f1 100%);
color: white; font-family: 'Vazirmatn', sans-serif;
font-size: 1.1rem; font-weight: 800; letter-spacing: 0.02em;
cursor: pointer; display: flex; align-items: center; justify-content: center;
gap: 10px; position: relative; overflow: hidden;
transition: all 0.3s cubic-bezier(0.34,1.2,0.64,1);
box-shadow: 0 8px 32px rgba(3,105,161,0.35), 0 1px 0 rgba(255,255,255,0.15) inset;
}
.translate-btn::before {
content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.18), transparent);
transition: left 0.5s ease;
}
.translate-btn:hover::before { left: 100%; }
.translate-btn:hover { transform: translateY(-2px); box-shadow: 0 14px 40px rgba(3,105,161,0.4); }
.translate-btn:active { transform: scale(0.98); }
.translate-btn:disabled { opacity: 0.85; cursor: not-allowed; transform: none; }
.btn-icon-wrap {
width: 34px; height: 34px; background: rgba(255,255,255,0.2);
border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1rem;
}
/* ===== SPINNER ===== */
.spinner {
width: 22px; height: 22px;
border: 2.5px solid rgba(255,255,255,0.3); border-top-color: white;
border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ===== SETTINGS ===== */
.settings-panel {
margin-top: 18px; border-radius: 18px;
background: rgba(248,250,252,0.7); border: 1.5px solid var(--border); overflow: hidden;
}
.settings-toggle {
display: flex; justify-content: space-between; align-items: center;
padding: 14px 18px; cursor: pointer; user-select: none; list-style: none;
}
.settings-toggle::-webkit-details-marker { display: none; }
.settings-label { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; font-weight: 700; color: var(--muted); }
.settings-label .badge {
background: linear-gradient(135deg, var(--sky-pale), #ede9fe);
color: var(--ocean); font-size: 0.65rem; padding: 2px 8px;
border-radius: 20px; font-weight: 800;
}
.settings-chevron { color: var(--muted); font-size: 0.75rem; transition: transform 0.3s ease; }
details[open] .settings-chevron { transform: rotate(180deg); }
.settings-body {
padding: 0 18px 18px; border-top: 1px solid var(--border);
display: flex; flex-direction: column; gap: 16px;
animation: fadeDown 0.3s ease both;
}
@keyframes fadeDown {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.voice-select-wrap { margin-top: 12px; }
.settings-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
@media (max-width: 500px) { .settings-row { grid-template-columns: 1fr; } }
.slider-group label {
display: flex; justify-content: space-between; align-items: center;
font-size: 0.73rem; font-weight: 700; color: var(--muted); margin-bottom: 6px;
}
.slider-group label span {
background: linear-gradient(135deg, var(--sky-deep), var(--lavender));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 800;
}
input[type="range"] {
-webkit-appearance: none; width: 100%; height: 4px;
border-radius: 4px; background: linear-gradient(to left, var(--sky-deep) 50%, #e2e8f0 50%);
outline: none; cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%;
background: white; border: 2.5px solid var(--sky-deep);
box-shadow: 0 2px 8px rgba(3,105,161,0.25); cursor: pointer; transition: transform 0.15s;
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
#voiceSelect {
width: 100%; appearance: none; -webkit-appearance: none;
background: white; border: 1.5px solid var(--border); border-radius: 12px;
padding: 11px 14px 11px 38px;
font-family: 'Vazirmatn', sans-serif; font-size: 0.85rem; font-weight: 600;
color: var(--text); outline: none; transition: all 0.2s; cursor: pointer;
}
#voiceSelect:focus { border-color: var(--sky); box-shadow: 0 0 0 4px rgba(56,189,248,0.1); }
/* ===== OUTPUT ===== */
.output-section {
margin-top: 20px; display: none; flex-direction: column; gap: 14px;
opacity: 0; transform: translateY(16px);
transition: opacity 0.45s ease, transform 0.45s cubic-bezier(0.34,1.2,0.64,1);
}
.output-section.visible { display: flex; opacity: 1; transform: translateY(0); }
.output-box {
position: relative;
background: linear-gradient(135deg, rgba(224,242,254,0.6), rgba(237,233,254,0.4));
border: 1.5px solid rgba(56,189,248,0.25); border-radius: 18px;
padding: 22px 18px 52px; min-height: 90px;
}
.output-chip {
position: absolute; top: -12px; right: 18px;
background: linear-gradient(135deg, var(--sky-deep), var(--lavender));
color: white; font-size: 0.7rem; font-weight: 800;
padding: 4px 14px; border-radius: 20px; letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(3,105,161,0.3);
}
.output-text {
font-size: 1.05rem; line-height: 1.9; color: var(--text);
white-space: pre-wrap; margin-top: 8px;
}
.copy-btn {
position: absolute; bottom: 14px; left: 14px;
width: 34px; height: 34px; border-radius: 50%;
background: white; border: 1.5px solid rgba(56,189,248,0.3);
display: flex; align-items: center; justify-content: center;
cursor: pointer; color: var(--sky-deep); font-size: 0.8rem;
transition: all 0.2s; box-shadow: 0 2px 10px rgba(3,105,161,0.1);
}
.copy-btn:hover { background: var(--sky-pale); transform: scale(1.1); }
/* ===== AUDIO PLAYER ===== */
.audio-player-wrap {
background: white; border: 1.5px solid var(--border); border-radius: 16px;
padding: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.04);
display: flex; flex-direction: column; gap: 14px;
}
.audio-player-row {
display: flex; align-items: center; gap: 8px; width: 100%;
}
audio { flex: 1; height: 38px; border-radius: 8px; outline: none; min-width: 0; }
/* ===== DOWNLOAD BUTTON ===== */
.download-btn {
align-self: center;
display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 12px 24px; border-radius: 14px; border: none;
background: linear-gradient(135deg, #0ea5e9, #6366f1);
color: white; font-family: 'Vazirmatn', sans-serif;
font-size: 0.85rem; font-weight: 800; cursor: pointer;
transition: all 0.25s cubic-bezier(0.34,1.3,0.64,1);
box-shadow: 0 4px 14px rgba(14,165,233,0.35);
white-space: nowrap; position: relative; overflow: hidden;
min-width: 200px;
}
.download-btn::before {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.15), transparent);
border-radius: 14px;
}
.download-btn:hover {
transform: translateY(-2px) scale(1.04);
box-shadow: 0 8px 22px rgba(14,165,233,0.45);
}
.download-btn:active { transform: scale(0.97); }
.download-btn .dl-icon {
width: 22px; height: 22px; border-radius: 50%;
background: rgba(255,255,255,0.25);
display: flex; align-items: center; justify-content: center;
font-size: 0.72rem; flex-shrink: 0;
}
/* ===== SECTION DIVIDER ===== */
.section-divider { display: flex; align-items: center; gap: 10px; margin: 4px 0; }
.section-divider::before, .section-divider::after {
content: ''; flex: 1; height: 1px;
background: linear-gradient(to right, transparent, var(--border), transparent);
}
.section-divider span { font-size: 0.65rem; font-weight: 800; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; }
/* ===== TOAST ===== */
.toast-container {
position: fixed; top: 20px; right: 50%; transform: translateX(50%);
z-index: 9999; display: flex; flex-direction: column; gap: 10px; pointer-events: none;
}
.toast {
background: white; border-radius: 14px; padding: 12px 20px;
font-size: 0.88rem; font-weight: 700;
display: flex; align-items: center; gap: 10px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12); border: 1.5px solid var(--border);
pointer-events: auto; animation: toastIn 0.4s cubic-bezier(0.34,1.56,0.64,1) both;
min-width: 240px; max-width: 90vw;
}
.toast.leaving { animation: toastOut 0.3s ease both; }
@keyframes toastIn { from { opacity:0; transform: translateY(-20px) scale(0.9); } to { opacity:1; transform: translateY(0) scale(1); } }
@keyframes toastOut { from { opacity:1; transform: translateY(0) scale(1); } to { opacity:0; transform: translateY(-12px) scale(0.92); } }
.toast-icon { width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.85rem; flex-shrink: 0; }
.toast.success .toast-icon { background: #dcfce7; color: #16a34a; }
.toast.error .toast-icon { background: #fee2e2; color: #dc2626; }
.toast.info .toast-icon { background: var(--sky-pale); color: var(--sky-deep); }
/* ===== FOOTER ===== */
footer { padding: 16px 0 28px; }
/* ===== SCROLLBAR ===== */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
</style>
</head>
<body>
<div class="bg-scene"></div>
<div class="toast-container" id="toastContainer"></div>
<div class="app-wrapper">
<!-- HEADER -->
<header>
<div class="logo-wrap">
<div class="logo-icon-wrap">
<div class="logo-icon-pulse"></div>
<div class="logo-icon-pulse2"></div>
<!-- Google Translate style SVG icon -->
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="lg2" x1="22" y1="24" x2="56" y2="58" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#0ea5e9"/>
<stop offset="100%" stop-color="#6366f1"/>
</linearGradient>
</defs>
<rect x="8" y="12" width="34" height="34" rx="8" fill="#e0f2fe" stroke="#0ea5e9" stroke-width="2.5"/>
<text x="25" y="36" fill="#0ea5e9" font-size="22" font-family="Arial, sans-serif" font-weight="900" text-anchor="middle">A</text>
<rect x="22" y="24" width="34" height="34" rx="8" fill="url(#lg2)" stroke="#ffffff" stroke-width="3"/>
<text x="39" y="48" fill="#ffffff" font-size="22" font-family="Arial, sans-serif" font-weight="bold" text-anchor="middle"></text>
</svg>
</div>
<div class="logo-text">
<span class="logo-title">آلفا مترجم</span>
<span class="logo-sub">Alpha Translator</span>
</div>
</div>
</header>
<!-- MAIN CARD -->
<div class="card">
<!-- Language Row -->
<div class="lang-row">
<div class="lang-group">
<div class="lang-label"><span class="dot"></span> زبان مبدأ</div>
<div class="select-wrap">
<select id="sourceLang">
<option value="شناسایی خودکار">🌍 شناسایی خودکار</option>
</select>
<i class="fa-solid fa-chevron-down select-arrow"></i>
</div>
</div>
<button class="swap-btn" id="swapBtn" title="تعویض زبان‌ها">
<i class="fa-solid fa-arrows-left-right"></i>
</button>
<div class="lang-group">
<div class="lang-label"><span class="dot" style="background:var(--lavender)"></span> زبان مقصد</div>
<div class="select-wrap">
<select id="targetLang"></select>
<i class="fa-solid fa-chevron-down select-arrow" style="color:var(--lavender)"></i>
</div>
</div>
</div>
<!-- Textarea -->
<div class="textarea-wrap" id="textareaWrap">
<textarea id="inputText" rows="5" placeholder="متن خود را برای ترجمه اینجا وارد کنید..."></textarea>
<button class="clear-btn" id="clearBtn" title="پاک کردن">
<i class="fa-solid fa-eraser"></i>
</button>
<div class="textarea-hint" id="textHint">
<i class="fa-solid fa-triangle-exclamation"></i> لطفاً متن را وارد کنید
</div>
</div>
<!-- Translate Button -->
<div class="translate-btn-wrap">
<button class="translate-btn" id="translateBtn">
<div class="btn-icon-wrap">
<i class="fa-solid fa-wand-magic-sparkles"></i>
</div>
<span id="btnText">ترجمه و پخش صدا</span>
</button>
</div>
<!-- Settings -->
<details class="settings-panel" id="settingsDetails">
<summary class="settings-toggle">
<div class="settings-label">
<i class="fa-solid fa-sliders" style="color:var(--sky-deep)"></i>
تنظیمات گوینده و صدا
<span class="badge">پیشرفته</span>
</div>
<i class="fa-solid fa-chevron-down settings-chevron"></i>
</summary>
<div class="settings-body">
<div class="voice-select-wrap">
<div class="lang-label" style="margin-bottom:8px;">
<i class="fa-solid fa-microphone" style="color:var(--sky-deep)"></i> انتخاب گوینده
</div>
<div class="select-wrap">
<select id="voiceSelect"></select>
<i class="fa-solid fa-chevron-down select-arrow"></i>
</div>
</div>
<div class="section-divider"><span>تنظیمات صدا</span></div>
<div class="settings-row">
<div class="slider-group">
<label>سرعت <span id="rateVal">0</span></label>
<input type="range" id="rateSlider" min="-50" max="50" value="0">
</div>
<div class="slider-group">
<label>گام <span id="pitchVal">0</span></label>
<input type="range" id="pitchSlider" min="-50" max="50" value="0">
</div>
<div class="slider-group">
<label>حجم <span id="volVal">0</span></label>
<input type="range" id="volSlider" min="-50" max="50" value="0">
</div>
</div>
</div>
</details>
<!-- Output -->
<div class="output-section" id="outputSection">
<div class="output-box">
<div class="output-chip">✦ ترجمه</div>
<p class="output-text" id="outputText"></p>
<button class="copy-btn" id="copyBtn" title="کپی متن">
<i class="fa-regular fa-copy"></i>
</button>
</div>
<div class="audio-player-wrap">
<div class="audio-player-row">
<audio id="audioPlayer" controls controlsList="nodownload">
مرورگر شما از پخش صدا پشتیبانی نمی‌کند.
</audio>
</div>
<button class="download-btn" id="downloadBtn" title="دانلود صدا">
<div class="dl-icon"><i class="fa-solid fa-arrow-down"></i></div>
دانلود صدا
</button>
</div>
</div>
</div><!-- end .card -->
<footer></footer>
</div>
<script>
// ===== RTL LANGUAGES MAP =====
const RTL_LANGS = new Set(['فارسی','عربی','هبری','اردو','پشتو','کردی','سندی','دیوهی']);
function isRTL(langName) {
return RTL_LANGS.has(langName);
}
// ===== TOAST =====
function showToast(msg, type = 'info', duration = 3500) {
const icons = { success: 'fa-check', error: 'fa-xmark', info: 'fa-info' };
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<div class="toast-icon"><i class="fa-solid ${icons[type]}"></i></div><span>${msg}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('leaving');
setTimeout(() => toast.remove(), 300);
}, duration);
}
// ===== SLIDERS =====
function updateSliderTrack(slider) {
const pct = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
slider.style.background = `linear-gradient(to left, var(--sky-deep) ${pct}%, #e2e8f0 ${pct}%)`;
}
['rate','pitch','vol'].forEach(id => {
const slider = document.getElementById(id+'Slider');
const valEl = document.getElementById(id+'Val');
updateSliderTrack(slider);
slider.addEventListener('input', () => { valEl.textContent = slider.value; updateSliderTrack(slider); });
});
// ===== ELEMENTS =====
let languagesData = {};
const sourceLang = document.getElementById('sourceLang');
const targetLang = document.getElementById('targetLang');
const voiceSelect = document.getElementById('voiceSelect');
const inputText = document.getElementById('inputText');
const translateBtn = document.getElementById('translateBtn');
const btnText = document.getElementById('btnText');
const outputSection = document.getElementById('outputSection');
const outputText = document.getElementById('outputText');
const audioPlayer = document.getElementById('audioPlayer');
const clearBtn = document.getElementById('clearBtn');
const copyBtn = document.getElementById('copyBtn');
const downloadBtn = document.getElementById('downloadBtn');
const textHint = document.getElementById('textHint');
// ===== TEXT DIRECTION HELPERS =====
function applyInputDir(langName) {
const rtl = isRTL(langName) || langName === 'شناسایی خودکار';
inputText.dir = rtl ? 'rtl' : 'ltr';
inputText.style.textAlign = rtl ? 'right' : 'left';
if (langName === 'شناسایی خودکار') {
checkDynamicDirection();
}
}
function checkDynamicDirection() {
if (sourceLang.value !== 'شناسایی خودکار') return;
const val = inputText.value;
const firstCharMatch = val.match(/\S/);
if (firstCharMatch) {
const rtlRegex = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
const isRtl = rtlRegex.test(firstCharMatch[0]);
inputText.dir = isRtl ? 'rtl' : 'ltr';
inputText.style.textAlign = isRtl ? 'right' : 'left';
} else {
inputText.dir = 'rtl';
inputText.style.textAlign = 'right';
}
}
inputText.addEventListener('input', checkDynamicDirection);
function applyOutputDir(langName) {
const rtl = isRTL(langName);
outputText.dir = rtl ? 'rtl' : 'ltr';
outputText.style.textAlign = rtl ? 'right' : 'left';
}
// ===== LOAD CONFIG =====
async function loadConfig() {
try {
const res = await fetch('/api/config');
const data = await res.json();
languagesData = data.languages;
Object.keys(languagesData).forEach(lang => {
sourceLang.add(new Option(lang, lang));
const isSelected = lang === 'انگلیسی';
targetLang.add(new Option(lang, lang, isSelected, isSelected));
});
updateVoices('انگلیسی');
applyInputDir('شناسایی خودکار');
applyOutputDir('انگلیسی');
} catch {
showToast('خطا در بارگذاری زبان‌ها', 'error');
}
}
function updateVoices(langKey) {
voiceSelect.innerHTML = '';
const voices = languagesData[langKey]?.voices || {};
if (!Object.keys(voices).length) { voiceSelect.add(new Option('بدون پشتیبانی صوتی', '')); return; }
Object.keys(voices).forEach(vName => voiceSelect.add(new Option(vName, vName)));
}
sourceLang.addEventListener('change', e => applyInputDir(e.target.value));
targetLang.addEventListener('change', e => { updateVoices(e.target.value); applyOutputDir(e.target.value); });
// ===== SWAP =====
document.getElementById('swapBtn').addEventListener('click', () => {
const sv = sourceLang.value, tv = targetLang.value;
if (sv === 'شناسایی خودکار') { showToast('ابتدا یک زبان مبدأ انتخاب کنید', 'info'); return; }
for (let i = 0; i < sourceLang.options.length; i++) if (sourceLang.options[i].value === tv) { sourceLang.selectedIndex = i; break; }
for (let i = 0; i < targetLang.options.length; i++) if (targetLang.options[i].value === sv) { targetLang.selectedIndex = i; break; }
updateVoices(targetLang.value);
applyInputDir(sourceLang.value);
applyOutputDir(targetLang.value);
const translated = outputText.textContent;
if (translated) { inputText.value = translated; outputSection.classList.remove('visible'); setTimeout(() => { outputSection.style.display = 'none'; }, 450); }
});
// ===== CLEAR =====
clearBtn.addEventListener('click', () => {
inputText.value = '';
inputText.focus();
checkDynamicDirection();
outputSection.classList.remove('visible');
setTimeout(() => { outputSection.style.display = 'none'; }, 450);
});
// ===== COPY =====
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(outputText.innerText).then(() => {
copyBtn.innerHTML = '<i class="fa-solid fa-check" style="color:#16a34a"></i>';
showToast('متن کپی شد!', 'success', 2000);
setTimeout(() => copyBtn.innerHTML = '<i class="fa-regular fa-copy"></i>', 2000);
});
});
// ===== DOWNLOAD =====
downloadBtn.addEventListener('click', () => {
const src = audioPlayer.src;
if (!src) return;
// بررسی اینکه آیا صفحه درون آی‌فریم باز شده است یا خیر
if (window.self !== window.top) {
// ارسال آدرس صدا به کامپوننت والد جهت آپلود تکه‌ای و دانلود با نام سایت شما
window.parent.postMessage({
type: 'INITIATE_DOWNLOAD_FROM_URL',
payload: {
audioUrl: src
}
}, '*');
showToast('در حال آماده‌سازی فایل صوتی در برنامه...', 'success', 2000);
} else {
// حالت پیش‌فرض (دانلود مستقیم مرورگر) در صورتی که خارج از آی‌فریم باز شده باشد
const a = document.createElement('a');
a.href = src;
a.download = 'alpha-translator-audio.mp3';
a.click();
showToast('در حال دانلود مستقیم...', 'success', 2000);
}
});
// ===== TRANSLATE =====
translateBtn.addEventListener('click', async () => {
const text = inputText.value.trim();
if (!text) {
inputText.classList.add('shake');
textHint.classList.add('show');
setTimeout(() => inputText.classList.remove('shake'), 500);
setTimeout(() => textHint.classList.remove('show'), 3000);
return;
}
textHint.classList.remove('show');
translateBtn.disabled = true;
btnText.innerHTML = '<span style="display:inline-flex; align-items:center; gap:8px;"><span class="spinner"></span> در حال پردازش...</span>';
outputSection.classList.remove('visible');
setTimeout(() => { outputSection.style.display = 'none'; }, 450);
try {
const response = await fetch('/api/translate_and_speak', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
source_lang: sourceLang.value,
target_lang: targetLang.value,
voice_key: voiceSelect.value || '',
rate: parseInt(document.getElementById('rateSlider').value),
pitch: parseInt(document.getElementById('pitchSlider').value),
volume: parseInt(document.getElementById('volSlider').value)
})
});
const result = await response.json();
if (result.success) {
outputText.innerText = result.translated_text;
applyOutputDir(targetLang.value);
if (result.audio_url) {
audioPlayer.src = result.audio_url + '?t=' + Date.now();
audioPlayer.parentElement.parentElement.style.display = 'flex';
audioPlayer.play().catch(() => {});
} else {
audioPlayer.parentElement.parentElement.style.display = 'none';
}
outputSection.style.display = 'flex';
requestAnimationFrame(() => requestAnimationFrame(() => outputSection.classList.add('visible')));
showToast('ترجمه با موفقیت انجام شد!', 'success');
} else {
showToast(result.error || 'خطایی رخ داد', 'error');
}
} catch {
showToast('خطا در ارتباط با سرور', 'error');
} finally {
translateBtn.disabled = false;
btnText.innerHTML = 'ترجمه و پخش صدا';
}
});
loadConfig();
</script>
</body>
</html>