// ======================================================================= // Notes Application - Dictionary Module (Native ES Module) // ======================================================================= import { api } from './api.js'; import { escapeHtml, showToast, openModal, closeModal, playAudio } from './ui.js'; export let allCards = []; export let collectionsList = ['General', 'Разговорный', 'Работа & IT', 'Книги & Сериалы', 'Идиомы']; export let selectedTag = ''; export let selectedCollection = ''; export let searchQuery = ''; let currentLookupData = null; let foundDuplicateCard = null; let duplicateCheckTimer = null; let isBatchMode = false; let selectedIds = new Set(); export function getAllCards() { return allCards; } export function initDictionary() { // Elements - Search & Capture const termInput = document.getElementById('term-input'); const clearSearchBtn = document.getElementById('clear-search-btn'); const translateBtn = document.getElementById('translate-btn'); const duplicateWarningBox = document.getElementById('duplicate-warning-box'); const dupTermLabel = document.getElementById('dup-term-label'); const dupTransLabel = document.getElementById('dup-trans-label'); const dupOpenBtn = document.getElementById('dup-open-btn'); const lookupCard = document.getElementById('lookup-card'); const resTerm = document.getElementById('res-term'); const resPhonetic = document.getElementById('res-phonetic'); const resInLibrary = document.getElementById('res-in-library'); const audioPlayBtn = document.getElementById('audio-play-btn'); const resPos = document.getElementById('res-pos'); const resDefinitionWrap = document.getElementById('res-definition-wrap'); const resDefinition = document.getElementById('res-definition'); const editTranslation = document.getElementById('edit-translation'); const editContext = document.getElementById('edit-context'); const editCollection = document.getElementById('edit-collection'); const collectionsDatalist = document.getElementById('collections-datalist'); const editTags = document.getElementById('edit-tags'); const editNotes = document.getElementById('edit-notes'); const saveCardBtn = document.getElementById('save-card-btn'); // Elements - Vocabulary List const filterInput = document.getElementById('filter-input'); const cardsCountLabel = document.getElementById('cards-count-label'); const collectionsFilterBar = document.getElementById('collections-filter-bar'); const newCollectionBtn = document.getElementById('new-collection-btn'); const tagsFilterBar = document.getElementById('tags-filter-bar'); const cardsList = document.getElementById('cards-list'); const emptyState = document.getElementById('empty-state'); // Elements - Batch Action Bar const batchToggleBtn = document.getElementById('batch-toggle-btn'); const batchActionBar = document.getElementById('batch-action-bar'); const batchSelectAll = document.getElementById('batch-select-all'); const batchCountLabel = document.getElementById('batch-count-label'); const batchMoveBtn = document.getElementById('batch-move-btn'); const batchTagBtn = document.getElementById('batch-tag-btn'); const batchDeleteBtn = document.getElementById('batch-delete-btn'); const batchCancelBtn = document.getElementById('batch-cancel-btn'); // Elements - Modals const editModal = document.getElementById('edit-card-modal'); const closeModalBtn = document.getElementById('close-modal-btn'); const modalCancelBtn = document.getElementById('modal-cancel-btn'); const modalSaveBtn = document.getElementById('modal-save-btn'); const modalCardId = document.getElementById('modal-card-id'); const modalTerm = document.getElementById('modal-term'); const modalTranslation = document.getElementById('modal-translation'); const modalContext = document.getElementById('modal-context'); const modalPhonetic = document.getElementById('modal-phonetic'); const modalPos = document.getElementById('modal-pos'); const modalDef = document.getElementById('modal-def'); const modalCollection = document.getElementById('modal-collection'); const modalTags = document.getElementById('modal-tags'); const modalNotes = document.getElementById('modal-notes'); const newColModal = document.getElementById('new-col-modal'); const newColNameInput = document.getElementById('new-col-name-input'); const closeNewColModalBtn = document.getElementById('close-new-col-modal-btn'); const newColCancelBtn = document.getElementById('new-col-cancel-btn'); const newColSaveBtn = document.getElementById('new-col-save-btn'); const moveColModal = document.getElementById('move-col-modal'); const moveColInput = document.getElementById('move-col-input'); const moveColQuickList = document.getElementById('move-col-quick-list'); const closeMoveModalBtn = document.getElementById('close-move-modal-btn'); const moveColCancelBtn = document.getElementById('move-col-cancel-btn'); const moveColConfirmBtn = document.getElementById('move-col-confirm-btn'); const exportOpenBtn = document.getElementById('export-open-btn'); const drawerNavExport = document.getElementById('drawer-nav-export'); const exportModal = document.getElementById('export-modal'); const closeExportModalBtn = document.getElementById('close-export-modal-btn'); const exportCancelBtn = document.getElementById('export-cancel-btn'); // ----------------------------------------------------------------------- // Duplicate Detection & Search Term // ----------------------------------------------------------------------- termInput.addEventListener('input', () => { const val = termInput.value.trim(); clearSearchBtn.classList.toggle('hidden', val.length === 0); clearTimeout(duplicateCheckTimer); if (val.length >= 2) { duplicateCheckTimer = setTimeout(async () => { try { const resp = await fetch(`/api/cards/check-duplicate?term=${encodeURIComponent(val)}`); if (resp.ok) { const res = await resp.json(); if (res.exists && res.card) { foundDuplicateCard = res.card; dupTermLabel.textContent = res.card.term; dupTransLabel.textContent = res.card.translation; duplicateWarningBox.classList.remove('hidden'); } else { foundDuplicateCard = null; duplicateWarningBox.classList.add('hidden'); } } } catch (e) {} }, 300); } else { foundDuplicateCard = null; duplicateWarningBox.classList.add('hidden'); } }); dupOpenBtn.addEventListener('click', () => { if (foundDuplicateCard) { openEditModal(foundDuplicateCard); } }); clearSearchBtn.addEventListener('click', () => { termInput.value = ''; clearSearchBtn.classList.add('hidden'); duplicateWarningBox.classList.add('hidden'); lookupCard.classList.add('hidden'); termInput.focus(); }); // ----------------------------------------------------------------------- // Dictionary Lookup & Capture // ----------------------------------------------------------------------- translateBtn.addEventListener('click', performLookup); termInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey) { e.preventDefault(); performLookup(); } else if (e.key === 'Escape') { termInput.value = ''; clearSearchBtn.classList.add('hidden'); duplicateWarningBox.classList.add('hidden'); lookupCard.classList.add('hidden'); } }); if (audioPlayBtn) { audioPlayBtn.addEventListener('click', () => { if (!currentLookupData || !currentLookupData.term) return; const url = currentLookupData.audio_url || `/api/tts?text=${encodeURIComponent(currentLookupData.term)}&lang=en`; playAudio(url); }); } async function performLookup() { const term = termInput.value.trim(); if (!term) return; translateBtn.disabled = true; translateBtn.textContent = 'Ищем...'; try { const data = await api.cards.lookup(term); currentLookupData = data; displayLookupResult(data); } catch (e) { console.error('Lookup error:', e); showToast('Не удалось получить перевод', 'error'); } finally { translateBtn.disabled = false; translateBtn.textContent = 'Перевести'; } } function displayLookupResult(data) { resTerm.textContent = data.term; resPhonetic.textContent = data.phonetic || ''; resPhonetic.style.display = data.phonetic ? 'inline-block' : 'none'; resPos.textContent = data.part_of_speech || 'Phrase'; if (audioPlayBtn) audioPlayBtn.classList.remove('hidden'); if (data.definition_en) { resDefinition.textContent = data.definition_en; resDefinitionWrap.classList.remove('hidden'); } else { resDefinitionWrap.classList.add('hidden'); } const inLib = Boolean(data.in_library || foundDuplicateCard); if (resInLibrary) { resInLibrary.classList.toggle('hidden', !inLib); } if (inLib) { saveCardBtn.innerHTML = '⭐ Обновить в словаре (Ctrl + Enter)'; const lib = data.library_card || foundDuplicateCard; if (lib) { if (lib.translation) editTranslation.value = lib.translation; if (lib.context_sentence) editContext.value = lib.context_sentence; if (lib.collection) editCollection.value = lib.collection; if (lib.tags) editTags.value = lib.tags; if (lib.notes) editNotes.value = lib.notes; } else { editTranslation.value = data.translation || ''; } } else { saveCardBtn.innerHTML = '💾 Сохранить в словарь (Ctrl + Enter)'; editTranslation.value = data.translation || ''; editContext.value = ''; editNotes.value = ''; editCollection.value = selectedCollection || 'General'; } lookupCard.classList.remove('hidden'); editTranslation.focus(); } // Save Card Action saveCardBtn.addEventListener('click', saveNewCard); document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { if (!lookupCard.classList.contains('hidden')) { e.preventDefault(); saveNewCard(); } } }); async function saveNewCard() { if (!currentLookupData) return; const term = currentLookupData.term.trim(); const translation = editTranslation.value.trim(); if (!translation) { alert('Пожалуйста, укажите русский перевод'); editTranslation.focus(); return; } const payload = { term: term, translation: translation, context_sentence: editContext.value.trim(), phonetic: currentLookupData.phonetic || '', audio_url: currentLookupData.audio_url || '', part_of_speech: currentLookupData.part_of_speech || '', definition_en: currentLookupData.definition_en || '', tags: editTags.value.trim() || 'general', notes: editNotes.value.trim(), collection: editCollection.value.trim() || 'General', }; saveCardBtn.disabled = true; saveCardBtn.textContent = 'Сохраняем...'; try { const resp = await api.cards.create(payload); if (resp.ok) { termInput.value = ''; clearSearchBtn.classList.add('hidden'); duplicateWarningBox.classList.add('hidden'); lookupCard.classList.add('hidden'); currentLookupData = null; showToast(`Слово "${term}" сохранено в словарь!`, 'success'); await loadInitialData(); termInput.focus(); } else { const err = await resp.json(); alert('Ошибка сохранения: ' + (err.detail || 'Не удалось сохранить')); } } catch (e) { alert('Сетевая ошибка при сохранении'); } finally { saveCardBtn.disabled = false; saveCardBtn.innerHTML = '💾 Сохранить в словарь (Ctrl + Enter)'; } } // ----------------------------------------------------------------------- // Cards List & Filtering // ----------------------------------------------------------------------- filterInput.addEventListener('input', () => { searchQuery = filterInput.value.trim(); fetchCards(); }); function renderCollectionsFilter(cols) { collectionsFilterBar.innerHTML = ''; const allBtn = document.createElement('button'); allBtn.className = `col-chip ${selectedCollection === '' ? 'active' : ''}`; allBtn.textContent = 'Все коллекции'; allBtn.onclick = () => { selectedCollection = ''; updateFilterChips(); fetchCards(); }; collectionsFilterBar.appendChild(allBtn); cols.forEach(c => { const chip = document.createElement('button'); chip.className = `col-chip ${selectedCollection === c.name ? 'active' : ''}`; chip.textContent = `${c.name} (${c.count})`; chip.onclick = () => { selectedCollection = c.name; updateFilterChips(); fetchCards(); }; collectionsFilterBar.appendChild(chip); }); } function renderTagsFilter(tags) { tagsFilterBar.innerHTML = ''; const allBtn = document.createElement('button'); allBtn.className = `filter-chip ${selectedTag === '' ? 'active' : ''}`; allBtn.textContent = 'Все теги'; allBtn.onclick = () => { selectedTag = ''; updateFilterChips(); fetchCards(); }; tagsFilterBar.appendChild(allBtn); tags.slice(0, 15).forEach(t => { const chip = document.createElement('button'); chip.className = `filter-chip ${selectedTag === t.tag ? 'active' : ''}`; chip.textContent = `#${t.tag} (${t.count})`; chip.onclick = () => { selectedTag = selectedTag === t.tag ? '' : t.tag; updateFilterChips(); fetchCards(); }; tagsFilterBar.appendChild(chip); }); } function updateFilterChips() { document.querySelectorAll('.col-chip').forEach(el => { el.classList.toggle('active', el.textContent.startsWith(selectedCollection || 'Все коллекции')); }); document.querySelectorAll('.filter-chip').forEach(el => { el.classList.toggle('active', el.textContent.startsWith(`#${selectedTag}`) || (!selectedTag && el.textContent.startsWith('Все теги'))); }); } function renderCardsList() { cardsCountLabel.textContent = `(${allCards.length} записей)`; cardsList.innerHTML = ''; if (allCards.length === 0) { emptyState.classList.remove('hidden'); cardsList.appendChild(emptyState); return; } emptyState.classList.add('hidden'); allCards.forEach(c => { const cardEl = document.createElement('div'); cardEl.className = `vocab-card ${isBatchMode && selectedIds.has(c.id) ? 'is-selected' : ''}`; const srsStateClass = c.srs_state === 'mastered' ? 'dot-mastered' : (c.srs_state === 'review' ? 'dot-review' : (c.srs_state === 'learning' ? 'dot-learning' : 'dot-new')); cardEl.innerHTML = `