434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
// Alias db.js functions
|
|
const dbGetAllEntries = getAllEntries;
|
|
const dbGetEntry = getEntry;
|
|
const dbAddEntry = addEntry;
|
|
const dbUpdateEntry = updateEntry;
|
|
const dbDeleteEntry = deleteEntry;
|
|
|
|
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 '<li class="' + active + '" data-id="' + e.id + '">' +
|
|
'<div class="entry-date">' + escHtml(e.date) + '</div>' +
|
|
'<div class="entry-preview">' + escHtml(preview) + '</div>' +
|
|
'<div class="entry-actions">' +
|
|
'<button class="btn-delete" data-id="' + e.id + '">🗑 删除</button>' +
|
|
'</div>' +
|
|
'</li>';
|
|
}).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 updateLiveTranslation() {
|
|
var text = document.getElementById('entry-text').value;
|
|
if (!text.trim()) {
|
|
clearLiveTranslation();
|
|
return;
|
|
}
|
|
var result = translateText(text);
|
|
document.getElementById('editor-translate-en').textContent = result.english;
|
|
document.getElementById('editor-translate-cn').textContent = result.chinese;
|
|
|
|
updateVocabulary(text);
|
|
}
|
|
|
|
function clearLiveTranslation() {
|
|
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 = '<div style="font-size:0.75rem;color:var(--text-muted);font-style:italic">输入西语后显示单词</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = vocab.map(function(v) {
|
|
return '<div class="vocab-item">' +
|
|
'<span class="vocab-word">' + escHtml(v.word) + '</span>' +
|
|
'<button class="vocab-speech" data-word="' + escHtml(v.word) + '" data-lang="es">' + '\uD83D\uDD0A' + '</button>' +
|
|
'<span class="vocab-en">' + escHtml(v.english) + '</span>' +
|
|
'<button class="vocab-speech" data-word="' + escHtml(v.word) + '" data-lang="en">🔊</button>' +
|
|
'<span class="vocab-zh">' + escHtml(v.chinese) + '</span>' +
|
|
'<button class="vocab-speech" data-word="' + escHtml(v.word) + '" data-lang="zh">🔊</button>' +
|
|
'</div>';
|
|
}).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 && selText.length > 0) {
|
|
var result = translateSelection(selText);
|
|
document.getElementById('selected-translation').textContent =
|
|
result.english + ' / ' + result.chinese;
|
|
el.style.display = 'block';
|
|
} else {
|
|
el.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ----- Event handlers -----
|
|
|
|
function selectEntry(id) {
|
|
state.currentId = id;
|
|
dbGetEntry(id).then(function(entry) {
|
|
if (entry) {
|
|
setEditor(entry);
|
|
renderTranslate(entry);
|
|
renderList();
|
|
}
|
|
});
|
|
}
|
|
|
|
function saveCurrentEntry() {
|
|
var date = document.getElementById('entry-date').value;
|
|
var spanish = document.getElementById('entry-text').value.trim();
|
|
if (!spanish) { alert('请输入西语内容'); return; }
|
|
|
|
var translation = translateText(spanish);
|
|
|
|
var savePromise;
|
|
if (state.currentId) {
|
|
savePromise = dbGetEntry(state.currentId).then(function(existing) {
|
|
existing.date = date;
|
|
existing.spanish = spanish;
|
|
existing.english = translation.english;
|
|
existing.chinese = translation.chinese;
|
|
existing.updatedAt = new Date().toISOString();
|
|
return dbUpdateEntry(existing);
|
|
});
|
|
} 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;
|
|
});
|
|
}
|
|
|
|
savePromise.then(function() {
|
|
return dbGetAllEntries();
|
|
}).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);
|
|
});
|
|
}
|
|
|
|
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) {
|
|
dbDeleteEntry(id).then(function() {
|
|
if (state.currentId === id) clearEditor();
|
|
return dbGetAllEntries();
|
|
}).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);
|
|
document.getElementById('helper-result').textContent = local || '本地词典未收录,正在尝试在线翻译...';
|
|
document.getElementById('helper-confirm').disabled = !local;
|
|
helperDebounceTimer = setTimeout(function() {
|
|
translateZhOnline(zh);
|
|
}, 600);
|
|
}
|
|
|
|
function translateZhOnline(zh) {
|
|
var seq = ++helperRequestSeq;
|
|
var url = 'https://api.mymemory.translated.net/get?q=' +
|
|
encodeURIComponent(zh) + '&langpair=zh-CN|es';
|
|
fetch(url)
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
if (seq !== helperRequestSeq) return;
|
|
var text = data && data.responseData && data.responseData.translatedText;
|
|
if (text && !data.quotaFinished) {
|
|
helperOnlineResult = text.trim();
|
|
document.getElementById('helper-result').textContent = helperOnlineResult;
|
|
document.getElementById('helper-confirm').disabled = false;
|
|
}
|
|
})
|
|
.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);
|
|
}
|
|
|
|
// ----- Initialization -----
|
|
|
|
function init() {
|
|
dbGetAllEntries().then(function(entries) {
|
|
state.entries = entries;
|
|
renderList();
|
|
setEditor(null);
|
|
renderTranslate(null);
|
|
});
|
|
|
|
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.querySelectorAll('.btn-speech').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { handleSpeech(btn.dataset.lang); });
|
|
});
|
|
|
|
// Live translation on input
|
|
document.getElementById('entry-text').addEventListener('input', updateLiveTranslation);
|
|
|
|
// Selection translation
|
|
document.getElementById('entry-text').addEventListener('mouseup', function(e) { handleSelection(); saveTextareaSelection(); });
|
|
document.getElementById('entry-text').addEventListener('keyup', function(e) { handleSelection(); saveTextareaSelection(); });
|
|
|
|
// Vocabulary speech delegation
|
|
document.getElementById('editor-vocab').addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.vocab-speech');
|
|
if (btn) handleVocabSpeech(btn.dataset.word, btn.dataset.lang);
|
|
});
|
|
|
|
// Vocabulary double-click to auto-read
|
|
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');
|
|
}
|
|
});
|
|
|
|
// Textarea double-click auto-read
|
|
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);
|
|
});
|
|
|
|
// Speak selected button - reads selection aloud
|
|
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();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|