FlatlyPage
Version 1.0.1 • 59 files • 798.19 KB
Files
.htaccess
.last_check
admin/account.php
admin/dashboard.php
admin/easyedit.js
admin/extensions.php
admin/generate-hash.php
admin/index.php
admin/lang.php
admin/login_tracking.php
admin/logout.php
admin/popups.php
admin/preview.php
admin/scripts.php
admin/theme-edit/index.php
admin/themes.php
api/get-translate-config.php
api/translate.php
assets/fonts/inter/inter.css
assets/fonts/space-grotesk/space-grotesk.css
assets/js/admin-editor.js
assets/js/admin-theme.js
assets/js/translate.js
config.php
contact-handler.php
contact.php
css/admin.css
css/contact.css
css/styles.css
css/theme.css
css/translate.css
data/.htaccess
data/.index.php
data/.settings.php
data/private/.htaccess
data/sitemap-config.php
data/translate-api.xml
engine/index.html
engine/index.php
engine/renderion.php
extensions-loader.php
favicons.txt
functions.php
index.php
newsletter/.htaccess
newsletter/confirm.php
newsletter/manager.php
newsletter/newsletter-form.js
newsletter/newsletter-styles.css
newsletter/newsletter-unavailable.php
newsletter/newsletter.sql
newsletter/settings.php
newsletter/subscribe.php
newsletter/unsubscribe.php
page.php
robots.txt.php
sitemap.php
updater/index.php
version.txt
assets/js/translate.js
(function() {
'use strict';
let lastSourceLang = localStorage.getItem('translator_source') || 'pl';
let lastTargetLang = localStorage.getItem('translator_target') || 'en';
let selectedAPI = localStorage.getItem('translator_api') || 'mymemory';
let apiConfig = null;
const LANGUAGES = {
'en': 'English', 'pl': 'Polish', 'de': 'German', 'es': 'Spanish',
'fr': 'French', 'it': 'Italian', 'pt': 'Portuguese', 'ru': 'Russian',
'ja': 'Japanese', 'zh': 'Chinese', 'ar': 'Arabic', 'nl': 'Dutch',
'sv': 'Swedish', 'no': 'Norwegian', 'da': 'Danish', 'fi': 'Finnish',
'cs': 'Czech', 'sk': 'Slovak', 'hu': 'Hungarian', 'ro': 'Romanian',
'tr': 'Turkish', 'uk': 'Ukrainian', 'ko': 'Korean'
};
// SVG Icons
const ICONS = {
translate: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>',
globe: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>',
search: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>',
settings: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m5.2-14.8L13.4 7.6m-2.8 8.8-3.8 3.8M23 12h-6m-6 0H1m17.8 5.2-3.8-3.8m-8.8-2.8L1.2 6.8"/></svg>'
};
const style = document.createElement('style');
style.textContent = `
.translate-btn {
position: absolute;
right: 10px;
top: 10px;
background: var(--bg-elevated);
border: 1px solid var(--border);
color: var(--text);
border-radius: var(--radius-sm);
padding: 6px 14px;
cursor: pointer;
font-size: 0.6875rem;
font-weight: 500;
opacity: 0;
transition: all var(--transition-smooth);
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
pointer-events: auto;
}
.translate-btn.below-position {
position: absolute;
top: calc(100% + 6px);
right: 10px;
transform: none;
}
.translate-btn svg {
flex-shrink: 0;
}
*:hover > .translate-btn,
textarea:focus ~ .translate-btn,
.translate-btn:hover {
opacity: 1;
transform: translateY(0);
}
.translate-btn.below-position:hover {
transform: translateY(0);
}
.translate-btn:hover {
border-color: var(--blue);
background: var(--bg-hover);
box-shadow: 0 0 15px rgba(59, 130, 246, 0.2);
}
.tr-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
animation: fadeIn var(--transition-base) ease-out;
}
[data-theme="light"] .tr-overlay {
background: rgba(15, 23, 42, 0.5);
}
.tr-modal {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius);
width: 100%;
max-width: 580px;
padding: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--text);
animation: modalSlideIn var(--transition-smooth) cubic-bezier(0.16, 1, 0.3, 1);
}
[data-theme="light"] .tr-modal {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.tr-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 20px 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.tr-title-left {
display: flex;
align-items: center;
gap: 10px;
}
.tr-title svg {
color: var(--blue);
}
.tr-search {
width: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text);
padding: 12px 12px 12px 40px;
border-radius: var(--radius-sm);
margin-bottom: 20px;
outline: none;
transition: border-color var(--transition-base);
font-size: 0.875rem;
}
.tr-search-wrapper {
position: relative;
}
.tr-search-icon {
position: absolute;
left: 12px;
top: 22px;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
z-index: 1;
}
.tr-search:focus {
border-color: var(--border-focus);
}
.tr-label {
font-size: 0.6875rem;
font-weight: 600;
color: var(--text-subtle);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 10px;
display: block;
}
.tr-api-selector {
display: flex;
gap: 8px;
margin-bottom: 20px;
padding: 4px;
background: var(--bg-card);
border-radius: var(--radius-sm);
}
.tr-api-option {
flex: 1;
padding: 10px;
border-radius: var(--radius-sm);
cursor: pointer;
text-align: center;
font-size: 0.8125rem;
color: var(--text-muted);
transition: all var(--transition-base);
border: 1px solid transparent;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.tr-api-option:hover {
color: var(--text);
background: var(--bg-hover);
}
.tr-api-option.selected {
background: rgba(59, 130, 246, 0.1);
border-color: var(--blue);
color: var(--text);
font-weight: 600;
}
.tr-api-badge {
display: inline-block;
font-size: 0.625rem;
padding: 2px 6px;
background: var(--bg-elevated);
border-radius: 4px;
margin-left: 0;
}
.tr-api-badge.free {
background: var(--success);
color: white;
}
.tr-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 8px;
max-height: 140px;
overflow-y: auto;
padding-right: 5px;
margin-bottom: 20px;
}
.tr-grid::-webkit-scrollbar {
width: 4px;
}
.tr-grid::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 10px;
}
.tr-grid::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
.tr-opt {
background: var(--bg-card);
border: 1px solid var(--border);
padding: 10px;
border-radius: var(--radius-sm);
cursor: pointer;
text-align: center;
font-size: 0.8125rem;
color: var(--text-muted);
transition: all var(--transition-base);
}
.tr-opt:hover {
background: var(--bg-hover);
color: var(--text);
border-color: var(--border-focus);
}
.tr-opt.selected {
background: rgba(59, 130, 246, 0.1);
border-color: var(--blue);
color: var(--text);
font-weight: 500;
box-shadow: inset 0 0 10px rgba(59, 130, 246, 0.1);
}
.tr-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.tr-btn-main {
padding: 12px 24px;
border-radius: var(--radius-sm);
border: none;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
font-size: 0.875rem;
}
.tr-btn-cancel {
background: transparent;
color: var(--text-muted);
}
.tr-btn-cancel:hover {
color: var(--text);
}
.tr-btn-confirm {
background: var(--blue);
color: white;
box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.39);
}
.tr-btn-confirm:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.23);
}
.tr-btn-confirm:active {
transform: scale(0.98);
}
.loader-spin {
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.tr-modal {
max-width: 100%;
border-radius: var(--radius) var(--radius) 0 0;
padding: 20px;
}
.tr-title {
font-size: 1.125rem;
}
.tr-api-option {
font-size: 0.75rem;
padding: 8px;
}
.tr-grid {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
}
.tr-footer {
flex-direction: column;
gap: 10px;
}
.tr-btn-main {
width: 100%;
}
}
`;
document.head.appendChild(style);
async function loadAPIConfig() {
try {
const response = await fetch('../api/get-translate-config');
return await response.json();
} catch (e) {
console.error('Failed to load API config:', e);
return {
'mymemory': {
name: 'MyMemory',
free: true,
rateLimit: '1000/day'
}
};
}
}
function showModal(callback) {
const overlay = document.createElement('div');
overlay.className = 'tr-overlay';
const apiButtons = apiConfig ? Object.entries(apiConfig).map(([id, config]) => `
<div class="tr-api-option ${id === selectedAPI ? 'selected' : ''}" data-api="${id}">
${config.name}
${config.free ? '<span class="tr-api-badge free">FREE</span>' : '<span class="tr-api-badge">PRO</span>'}
</div>
`).join('') : '';
overlay.innerHTML = `
<div class="tr-modal">
<div class="tr-title">
<div class="tr-title-left">
${ICONS.globe} Smart Translator
</div>
</div>
${apiConfig ? `
<div class="tr-label">Translation Provider</div>
<div class="tr-api-selector">
${apiButtons}
</div>
` : ''}
<div class="tr-search-wrapper">
<span class="tr-search-icon">${ICONS.search}</span>
<input type="text" class="tr-search" placeholder="Search languages...">
</div>
<span class="tr-label">Source Language</span>
<div class="tr-grid" id="src-grid"></div>
<span class="tr-label">Target Language</span>
<div class="tr-grid" id="trg-grid"></div>
<div class="tr-footer">
<button class="tr-btn-main tr-btn-cancel">Close</button>
<button class="tr-btn-main tr-btn-confirm">Translate Content</button>
</div>
</div>`;
document.body.appendChild(overlay);
let sSrc = lastSourceLang;
let sTrg = lastTargetLang;
let sAPI = selectedAPI;
overlay.querySelectorAll('.tr-api-option').forEach(opt => {
opt.onclick = () => {
sAPI = opt.dataset.api;
overlay.querySelectorAll('.tr-api-option').forEach(o => o.classList.remove('selected'));
opt.classList.add('selected');
};
});
const render = (filter = '') => {
const srcG = overlay.querySelector('#src-grid');
const trgG = overlay.querySelector('#trg-grid');
srcG.innerHTML = ''; trgG.innerHTML = '';
Object.entries(LANGUAGES).forEach(([code, name]) => {
if (!name.toLowerCase().includes(filter.toLowerCase())) return;
[ {g: srcG, cur: sSrc, set: (c)=>sSrc=c}, {g: trgG, cur: sTrg, set: (c)=>sTrg=c} ].forEach(config => {
const el = document.createElement('div');
el.className = `tr-opt ${code === config.cur ? 'selected' : ''}`;
el.textContent = name;
el.onclick = () => { config.set(code); render(filter); };
config.g.appendChild(el);
});
});
};
overlay.querySelector('.tr-search').oninput = (e) => render(e.target.value);
overlay.querySelector('.tr-btn-cancel').onclick = () => overlay.remove();
overlay.querySelector('.tr-btn-confirm').onclick = () => {
lastSourceLang = sSrc; lastTargetLang = sTrg; selectedAPI = sAPI;
localStorage.setItem('translator_source', sSrc);
localStorage.setItem('translator_target', sTrg);
localStorage.setItem('translator_api', sAPI);
overlay.remove();
callback(sSrc, sTrg, sAPI);
};
render();
}
async function translateText(text, src, trg, api) {
const response = await fetch('../api/translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: text,
source: src,
target: trg,
provider: api
})
});
if (!response.ok) {
const error = await response.json();
console.error('❌ Backend error:', error);
throw new Error(error.error || 'Translation failed');
}
const data = await response.json();
return data.translatedText;
}
async function handleTranslate(field, btn) {
const val = field.value.trim();
if (!val) {
alert('Please enter text to translate');
return;
}
showModal(async (src, trg, api) => {
const original = btn.innerHTML;
btn.innerHTML = `<div class="loader-spin"></div>`;
btn.classList.add('loading');
btn.disabled = true;
try {
const translated = await translateText(val, src, trg, api);
if (translated) {
field.value = translated;
field.dispatchEvent(new Event('input', { bubbles: true }));
} else {
alert('Translation failed or limit exceeded');
}
} catch (e) {
console.error('❌ Translation error:', e);
alert('Connection failed: ' + e.message);
} finally {
btn.innerHTML = original;
btn.classList.remove('loading');
btn.disabled = false;
}
});
}
function hasBlockActions(field) {
const parent = field.parentElement;
return parent && parent.querySelector('.block-actions') !== null;
}
function inject(field) {
if (field.dataset.hasTr === 'true' ||
field.type === 'hidden' ||
field.readOnly ||
field.closest('.tr-modal')) {
return;
}
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'translate-btn';
if (hasBlockActions(field)) {
btn.classList.add('below-position');
}
btn.innerHTML = `${ICONS.translate} Translate`;
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
handleTranslate(field, btn);
};
if (getComputedStyle(field.parentElement).position === 'static') {
field.parentElement.style.position = 'relative';
}
field.after(btn);
field.dataset.hasTr = 'true';
}
(async function init() {
apiConfig = await loadAPIConfig();
const scan = () => document.querySelectorAll('textarea, input[type="text"]').forEach(inject);
setInterval(scan, 3000);
scan();
})();
})();