// Alias db.js functions
const dbGetAllEntries = getAllEntries;
const dbGetEntry = getEntry;
const dbAddEntry = addEntry;
const dbUpdateEntry = updateEntry;
const dbDeleteEntry = deleteEntry;
var BACKEND_MODE = false;
var currentUser = null;
var favoriteWords = {};
var currentTranslationSettings = { translator: 'local' };
var liveTranslationTimer = null;
var liveTranslationSeq = 0;
var selectionTimer = null;
var selectionSeq = 0;
function safeGetAllEntries() {
if (BACKEND_MODE) return apiGetEntries();
return dbGetAllEntries();
}
function safeAddEntry(entry) {
if (BACKEND_MODE) return apiAddEntry(entry);
return dbAddEntry(entry);
}
function safeUpdateEntry(id, entry) {
if (BACKEND_MODE) return apiUpdateEntry(id, entry);
return dbGetEntry(id).then(function(existing) {
Object.assign(existing, entry);
existing.updatedAt = new Date().toISOString();
return dbUpdateEntry(existing);
});
}
function safeDeleteEntry(id) {
if (BACKEND_MODE) return apiDeleteEntry(id);
return dbDeleteEntry(id);
}
var savedSelStart = 0;
var savedSelEnd = 0;
let state = {
entries: [],
currentId: null
};
function uuid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function todayStr() {
const d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
// ----- Render functions -----
function renderList() {
const ul = document.getElementById('entry-list');
ul.innerHTML = state.entries.map(e => {
const preview = e.spanish.slice(0, 30) + (e.spanish.length > 30 ? '...' : '');
const active = e.id === state.currentId ? 'active' : '';
return '
' +
'' + escHtml(e.date) + '
' +
'' + escHtml(preview) + '
' +
'' +
'' +
'
' +
'';
}).join('');
}
function escHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function renderTranslate(entry) {
document.getElementById('translate-en').textContent = entry ? entry.english : '-';
document.getElementById('translate-cn').textContent = entry ? entry.chinese : '-';
}
function setEditor(entry) {
document.getElementById('entry-date').value = entry ? entry.date : todayStr();
document.getElementById('entry-text').value = entry ? entry.spanish : '';
const hint = document.getElementById('entry-hint');
hint.textContent = entry && entry.spanish
? '已保存。修改后点击保存更新。'
: '输入西语后实时显示翻译';
updateLiveTranslation();
}
function clearEditor() {
state.currentId = null;
document.querySelectorAll('#entry-list li').forEach(function(el) { el.classList.remove('active'); });
setEditor(null);
renderTranslate(null);
clearLiveTranslation();
}
// ----- Live translation & vocabulary -----
function shouldUseApiTranslation() {
return BACKEND_MODE && (currentTranslationSettings.translator || 'local') !== 'local';
}
function translateEditorToEnZh(text) {
if (!shouldUseApiTranslation()) {
var local = translateText(text);
return Promise.resolve({ english: local.english, chinese: local.chinese, provider: 'local' });
}
return Promise.all([
apiTranslate({ text: text, source: 'es', target: 'en' }),
apiTranslate({ text: text, source: 'es', target: 'zh' })
]).then(function(results) {
if (!results[0].translatedText || !results[1].translatedText) {
throw new Error('翻译接口未返回结果');
}
return {
english: results[0].translatedText,
chinese: results[1].translatedText,
provider: 'api'
};
}).catch(function() {
var local = translateText(text);
return { english: local.english, chinese: local.chinese, provider: 'local' };
});
}
function updateLiveTranslation() {
var text = document.getElementById('entry-text').value;
if (!text.trim()) {
clearLiveTranslation();
return;
}
updateVocabulary(text);
if (!shouldUseApiTranslation()) {
var result = translateText(text);
document.getElementById('editor-translate-en').textContent = result.english;
document.getElementById('editor-translate-cn').textContent = result.chinese;
return;
}
if (liveTranslationTimer) clearTimeout(liveTranslationTimer);
liveTranslationTimer = setTimeout(function() {
fetchLiveTranslation(text);
}, 350);
}
function fetchLiveTranslation(text) {
var seq = ++liveTranslationSeq;
translateEditorToEnZh(text).then(function(result) {
if (seq !== liveTranslationSeq) return;
if (document.getElementById('entry-text').value !== text) return;
document.getElementById('editor-translate-en').textContent = result.english;
document.getElementById('editor-translate-cn').textContent = result.chinese;
});
}
function clearLiveTranslation() {
liveTranslationSeq++;
if (liveTranslationTimer) clearTimeout(liveTranslationTimer);
selectionSeq++;
if (selectionTimer) clearTimeout(selectionTimer);
document.getElementById('editor-translate-en').textContent = '-';
document.getElementById('editor-translate-cn').textContent = '-';
document.getElementById('vocab-list').innerHTML = '';
document.getElementById('selection-translate').style.display = 'none';
}
function updateVocabulary(text) {
var vocab = getVocabulary(text);
var container = document.getElementById('vocab-list');
if (vocab.length === 0) {
container.innerHTML = '输入西语后显示单词
';
return;
}
container.innerHTML = vocab.map(function(v) {
var isFavorite = favoriteWords[v.word.toLowerCase()] ? 'active' : '';
var favBtn = BACKEND_MODE
? ''
: '';
return '' +
'' + escHtml(v.word) + '' +
'' +
'' + escHtml(v.english) + '' +
'' +
'' + escHtml(v.chinese) + '' +
'' +
favBtn +
'
';
}).join('');
}
function handleVocabSpeech(word, lang) {
var entry = getVocabForWord(word);
if (entry) {
if (lang === 'en') speakText(entry.en, 'en');
else if (lang === 'zh') speakText(entry.zh, 'zh');
else speakText(word, 'es');
}
}
function handleSelection() {
var ta = document.getElementById('entry-text');
var selText = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim();
var el = document.getElementById('selection-translate');
if (!selText) {
selectionSeq++;
if (selectionTimer) clearTimeout(selectionTimer);
el.style.display = 'none';
return;
}
if (!shouldUseApiTranslation()) {
var result = translateSelection(selText);
document.getElementById('selected-translation').textContent =
result.english + ' / ' + result.chinese;
el.style.display = 'block';
return;
}
if (selectionTimer) clearTimeout(selectionTimer);
selectionTimer = setTimeout(function() {
fetchSelectionTranslation(selText);
}, 250);
}
function fetchSelectionTranslation(selText) {
var seq = ++selectionSeq;
translateEditorToEnZh(selText).then(function(result) {
if (seq !== selectionSeq) return;
var ta = document.getElementById('entry-text');
var current = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim();
if (current !== selText) return;
document.getElementById('selected-translation').textContent =
result.english + ' / ' + result.chinese;
document.getElementById('selection-translate').style.display = 'block';
});
}
// ----- Event handlers -----
function selectEntry(id) {
state.currentId = id;
var entry = state.entries.find(function(e) { return e.id == id; });
var p = entry ? Promise.resolve(entry) : dbGetEntry(id);
p.then(function(found) {
if (!found) return;
setEditor(found);
renderTranslate(found);
renderList();
});
}
function saveCurrentEntry() {
var date = document.getElementById('entry-date').value;
var spanish = document.getElementById('entry-text').value.trim();
if (!spanish) { alert('请输入西语内容'); return; }
var saveBtn = document.getElementById('btn-save');
saveBtn.textContent = '\u23F3 翻译中...';
translateEditorToEnZh(spanish).then(function(translation) {
var data = {
date: date,
spanish: spanish,
english: translation.english,
chinese: translation.chinese
};
var savePromise;
if (state.currentId) {
savePromise = safeUpdateEntry(state.currentId, data);
} else if (BACKEND_MODE) {
savePromise = apiAddEntry(data).then(function(entry) {
state.currentId = entry.id;
});
} else {
var entry = {
id: uuid(),
date: date,
spanish: spanish,
english: translation.english,
chinese: translation.chinese,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
savePromise = dbAddEntry(entry).then(function() {
state.currentId = entry.id;
});
}
return savePromise.then(function() {
return safeGetAllEntries();
}).then(function(entries) {
state.entries = entries;
renderList();
renderTranslate({ english: translation.english, chinese: translation.chinese });
var found = entries.find(function(e) { return e.id === state.currentId; });
if (found) setEditor(found);
document.getElementById('btn-save').textContent = '\uD83D\uDCBE 已保存 \u2713';
setTimeout(function() { document.getElementById('btn-save').textContent = '\uD83D\uDCBE 保存'; }, 1500);
});
}).catch(function() {
document.getElementById('btn-save').textContent = '\uD83D\uDCBE 保存';
});
}
function newEntry() {
clearEditor();
document.getElementById('entry-date').value = todayStr();
document.getElementById('entry-text').focus();
}
// ----- Modal -----
var pendingDeleteId = null;
function showDeleteModal(id) {
pendingDeleteId = id;
document.getElementById('modal-overlay').classList.add('show');
}
function hideDeleteModal() {
pendingDeleteId = null;
document.getElementById('modal-overlay').classList.remove('show');
}
function confirmDelete(id) {
safeDeleteEntry(id).then(function() {
if (state.currentId === id) clearEditor();
return safeGetAllEntries();
}).then(function(entries) {
state.entries = entries;
renderList();
hideDeleteModal();
});
}
function saveTextareaSelection() {
var ta = document.getElementById('entry-text');
savedSelStart = ta.selectionStart;
savedSelEnd = ta.selectionEnd;
}
// ----- Chinese to Spanish helper -----
var helperDebounceTimer = null;
var helperRequestSeq = 0;
var helperOnlineResult = '';
function openHelperModal() {
var ta = document.getElementById('entry-text');
savedSelStart = ta.selectionStart;
savedSelEnd = ta.selectionEnd;
helperOnlineResult = '';
if (helperDebounceTimer) clearTimeout(helperDebounceTimer);
document.getElementById('helper-input').value = '';
document.getElementById('helper-result').textContent = '';
document.getElementById('helper-confirm').disabled = true;
document.getElementById('helper-modal-overlay').classList.add('show');
setTimeout(function() { document.getElementById('helper-input').focus(); }, 0);
}
function closeHelperModal() {
if (helperDebounceTimer) clearTimeout(helperDebounceTimer);
helperRequestSeq++;
document.getElementById('helper-modal-overlay').classList.remove('show');
}
function updateHelperPreview() {
var zh = document.getElementById('helper-input').value;
helperOnlineResult = '';
if (helperDebounceTimer) clearTimeout(helperDebounceTimer);
if (!zh.trim()) {
document.getElementById('helper-result').textContent = '';
document.getElementById('helper-confirm').disabled = true;
return;
}
var local = translateZhToEs(zh);
if (shouldUseApiTranslation()) {
document.getElementById('helper-result').textContent = local || '正在调用已配置的翻译接口...';
document.getElementById('helper-confirm').disabled = true;
} else {
document.getElementById('helper-result').textContent = local || (BACKEND_MODE ? '本地词典未收录' : '本地词典未收录,正在尝试在线翻译...');
document.getElementById('helper-confirm').disabled = !local;
}
if (shouldUseApiTranslation() || !BACKEND_MODE) {
helperDebounceTimer = setTimeout(function() {
translateZhOnline(zh);
}, 600);
}
}
function translateZhOnline(zh) {
var seq = ++helperRequestSeq;
var request;
if (shouldUseApiTranslation()) {
request = apiTranslate({ text: zh, source: 'zh', target: 'es' }).then(function(data) {
return { translatedText: data.translatedText };
});
} else if (!BACKEND_MODE) {
var url = 'https://api.mymemory.translated.net/get?q=' +
encodeURIComponent(zh) + '&langpair=zh-CN|es';
request = fetch(url)
.then(function(res) { return res.json(); })
.then(function(data) {
return {
translatedText: data && data.responseData && data.responseData.translatedText,
quotaFinished: data && data.quotaFinished
};
});
} else {
var localOnly = translateZhToEs(zh);
if (localOnly) {
document.getElementById('helper-result').textContent = localOnly;
document.getElementById('helper-confirm').disabled = false;
}
return;
}
request.then(function(result) {
if (seq !== helperRequestSeq) return;
var text = result && result.translatedText;
if (text && !result.quotaFinished) {
helperOnlineResult = text.trim();
document.getElementById('helper-result').textContent = helperOnlineResult;
document.getElementById('helper-confirm').disabled = false;
} else {
var local = translateZhToEs(zh);
if (local) {
document.getElementById('helper-result').textContent = local;
document.getElementById('helper-confirm').disabled = false;
} else {
document.getElementById('helper-result').textContent = '翻译接口未返回结果,且本地词典未收录';
}
}
})
.catch(function() {
if (seq !== helperRequestSeq) return;
var local = translateZhToEs(zh);
if (local) {
document.getElementById('helper-result').textContent = local;
document.getElementById('helper-confirm').disabled = false;
} else {
document.getElementById('helper-result').textContent = '在线翻译暂不可用,且本地词典未收录';
}
});
}
function getHelperResult() {
if (helperOnlineResult) return helperOnlineResult;
return translateZhToEs(document.getElementById('helper-input').value);
}
function insertSpanishAtCursor(es) {
var ta = document.getElementById('entry-text');
var start = savedSelStart || ta.selectionStart;
var end = savedSelEnd || ta.selectionEnd;
var before = ta.value.slice(0, start);
var after = ta.value.slice(end);
var prefix = '';
var suffix = '';
if (before.length > 0 && !/\s$/.test(before)) prefix = ' ';
if (after.length > 0 && !/^\s/.test(after)) suffix = ' ';
var insert = prefix + es + suffix;
var newPos = start + insert.length;
ta.value = before + insert + after;
ta.focus();
ta.setSelectionRange(newPos, newPos);
savedSelStart = savedSelEnd = newPos;
updateLiveTranslation();
}
function confirmHelperInsert() {
var es = getHelperResult();
if (!es) return;
insertSpanishAtCursor(es);
closeHelperModal();
}
// ----- Speech handler -----
function handleSpeech(lang) {
var text = lang === 'en'
? document.getElementById('translate-en').textContent
: document.getElementById('translate-cn').textContent;
if (text && text !== '-') speakText(text, lang);
}
// ----- Backend features -----
function renderUserArea() {
if (!currentUser) return;
document.getElementById('user-name').textContent = currentUser.displayName || currentUser.username;
document.getElementById('btn-logout').style.display = '';
document.getElementById('btn-change-password').style.display = '';
if (currentUser.isAdmin) {
document.getElementById('admin-link').style.display = '';
}
}
function loadBackendData() {
safeGetAllEntries().then(function(entries) {
state.entries = entries;
renderList();
setEditor(null);
renderTranslate(null);
});
loadStats();
loadFavoriteWords();
loadSettingsForm();
}
function loadLocalData() {
safeGetAllEntries().then(function(entries) {
state.entries = entries;
renderList();
setEditor(null);
renderTranslate(null);
});
}
function loadStats() {
if (!BACKEND_MODE) {
document.getElementById('stats-content').textContent = '登录后显示统计';
return;
}
apiGetStats().then(function(stats) {
var dailyHtml = (stats.daily || []).map(function(d) {
return d.date + ': ' + d.count + ' 篇';
}).join(' · ');
document.getElementById('stats-content').innerHTML =
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'最近 7 天:' + escHtml(dailyHtml) + '
' +
'累计西语字符:' + stats.totalChars + '
';
}).catch(function() {});
}
function loadFavoriteWords() {
if (!BACKEND_MODE) return;
apiGetVocabulary().then(function(items) {
favoriteWords = {};
items.forEach(function(item) {
favoriteWords[item.word.toLowerCase()] = item;
});
updateLiveTranslation();
}).catch(function() {});
}
function toggleFavorite(btn) {
var word = btn.dataset.word;
var key = word.toLowerCase();
if (favoriteWords[key]) {
apiDeleteVocabulary(favoriteWords[key].id).then(function() {
delete favoriteWords[key];
btn.classList.remove('active');
}).catch(function(err) { alert(err.message); });
} else {
apiAddVocabulary({
word: word,
english: btn.dataset.en || '',
chinese: btn.dataset.zh || '',
category: ''
}).then(function(item) {
favoriteWords[key] = item;
btn.classList.add('active');
}).catch(function(err) { alert(err.message); });
}
}
function loadSettingsForm() {
if (!BACKEND_MODE) return;
apiGetTranslationSettings().then(function(settings) {
currentTranslationSettings = settings;
document.getElementById('translator-type').value = settings.translator || 'local';
document.getElementById('api-url').value = settings.api_url || '';
document.getElementById('api-key').value = settings.api_key || '';
document.getElementById('extra-config').value = JSON.stringify(settings.extra_config || {}, null, 2);
}).catch(function() {});
}
function openSettingsModal() {
document.getElementById('settings-modal-overlay').classList.add('show');
}
function closeSettingsModal() {
document.getElementById('settings-modal-overlay').classList.remove('show');
}
function openPasswordModal() {
document.getElementById('password-msg').textContent = '';
document.getElementById('password-old').value = '';
document.getElementById('password-new').value = '';
document.getElementById('password-confirm').value = '';
document.getElementById('password-modal-overlay').classList.add('show');
setTimeout(function() { document.getElementById('password-old').focus(); }, 0);
}
function closePasswordModal() {
document.getElementById('password-modal-overlay').classList.remove('show');
}
function savePasswordModal() {
var oldPassword = document.getElementById('password-old').value;
var newPassword = document.getElementById('password-new').value;
var confirmPassword = document.getElementById('password-confirm').value;
var msg = document.getElementById('password-msg');
if (!oldPassword) {
msg.textContent = '请输入当前密码';
return;
}
if (newPassword.length < 6) {
msg.textContent = '新密码至少 6 位';
return;
}
if (newPassword !== confirmPassword) {
msg.textContent = '两次输入的新密码不一致';
return;
}
apiChangePassword({
oldPassword: oldPassword,
newPassword: newPassword
}).then(function() {
closePasswordModal();
alert('密码已修改');
}).catch(function(err) {
msg.textContent = err.message || '修改密码失败';
});
}
function saveSettingsModal() {
var extra = {};
var rawExtra = document.getElementById('extra-config').value.trim();
if (rawExtra) {
try {
extra = JSON.parse(rawExtra);
} catch (e) {
alert('扩展配置必须是合法 JSON');
return;
}
}
apiSaveTranslationSettings({
translator: document.getElementById('translator-type').value,
api_url: document.getElementById('api-url').value.trim(),
api_key: document.getElementById('api-key').value.trim(),
extra_config: extra
}).then(function(saved) {
currentTranslationSettings = saved;
closeSettingsModal();
alert('翻译设置已保存');
}).catch(function(err) { alert(err.message); });
}
function handleLogout() {
apiLogout().then(function() {
location.href = 'login.html';
}).catch(function() {
location.href = 'login.html';
});
}
// ----- Initialization -----
function bindAppEvents() {
document.getElementById('entry-list').addEventListener('click', function(e) {
var li = e.target.closest('li[data-id]');
if (!li) return;
if (e.target.closest('.btn-delete')) {
showDeleteModal(li.dataset.id);
return;
}
selectEntry(li.dataset.id);
});
document.getElementById('btn-new').addEventListener('click', newEntry);
document.getElementById('btn-save').addEventListener('click', saveCurrentEntry);
document.getElementById('btn-helper').addEventListener('click', openHelperModal);
document.getElementById('helper-cancel').addEventListener('click', closeHelperModal);
document.getElementById('helper-confirm').addEventListener('click', confirmHelperInsert);
document.getElementById('helper-input').addEventListener('input', updateHelperPreview);
document.getElementById('btn-settings').addEventListener('click', openSettingsModal);
document.getElementById('settings-cancel').addEventListener('click', closeSettingsModal);
document.getElementById('settings-save').addEventListener('click', saveSettingsModal);
document.getElementById('btn-change-password').addEventListener('click', openPasswordModal);
document.getElementById('password-cancel').addEventListener('click', closePasswordModal);
document.getElementById('password-save').addEventListener('click', savePasswordModal);
document.getElementById('btn-logout').addEventListener('click', handleLogout);
document.querySelectorAll('#mobile-nav button').forEach(function(btn) {
btn.addEventListener('click', function() {
document.body.dataset.view = btn.dataset.view;
document.querySelectorAll('#mobile-nav button').forEach(function(b) {
b.classList.toggle('active', b === btn);
});
});
});
document.querySelectorAll('.btn-speech').forEach(function(btn) {
btn.addEventListener('click', function() { handleSpeech(btn.dataset.lang); });
});
document.getElementById('entry-text').addEventListener('input', updateLiveTranslation);
document.getElementById('entry-text').addEventListener('mouseup', function(e) { handleSelection(); saveTextareaSelection(); });
document.getElementById('entry-text').addEventListener('keyup', function(e) { handleSelection(); saveTextareaSelection(); });
document.getElementById('editor-vocab').addEventListener('click', function(e) {
var fav = e.target.closest('.favorite-btn');
if (fav) { toggleFavorite(fav); return; }
var btn = e.target.closest('.vocab-speech');
if (btn) handleVocabSpeech(btn.dataset.word, btn.dataset.lang);
});
document.getElementById('editor-vocab').addEventListener('dblclick', function(e) {
var item = e.target.closest('.vocab-item');
if (!item) return;
var word = item.querySelector('.vocab-word').textContent;
var el = e.target;
if (el.classList.contains('vocab-en') || el.closest('.vocab-en')) {
speakText(item.querySelector('.vocab-en').textContent, 'en');
} else if (el.classList.contains('vocab-zh') || el.closest('.vocab-zh')) {
speakText(item.querySelector('.vocab-zh').textContent, 'zh');
} else {
speakText(word, 'es');
}
});
document.getElementById('entry-text').addEventListener('dblclick', function() {
var ta = document.getElementById('entry-text');
setTimeout(function() {
var txt = ta.value.substring(ta.selectionStart, ta.selectionEnd).trim();
if (txt) speakText(txt, 'es');
}, 50);
});
document.getElementById('btn-speak-selected').addEventListener('click', function() {
var ta = document.getElementById('entry-text');
var start = savedSelStart || ta.selectionStart;
var end = savedSelEnd || ta.selectionEnd;
if (start === end) { start = 0; end = ta.value.length; }
var sel = ta.value.substring(start, end).trim();
if (sel) {
speakText(sel, 'es');
document.getElementById('selected-translation').textContent = sel;
document.getElementById('selection-translate').style.display = 'block';
}
});
document.querySelector('#modal-box .btn-cancel').addEventListener('click', hideDeleteModal);
document.querySelector('#modal-box .btn-confirm').addEventListener('click', function() {
if (pendingDeleteId) confirmDelete(pendingDeleteId);
});
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') saveCurrentEntry();
});
}
function startApp() {
if (location.protocol === 'file:') {
BACKEND_MODE = false;
loadLocalData();
return;
}
apiGetMe().then(function(me) {
BACKEND_MODE = true;
currentUser = me;
renderUserArea();
loadBackendData();
}).catch(function(err) {
if (err && err.status === 401) {
location.href = 'login.html';
return;
}
BACKEND_MODE = false;
loadLocalData();
});
}
function init() {
bindAppEvents();
startApp();
}
document.addEventListener('DOMContentLoaded', init);