// ======================================================================= // Notes Application - Dictionary Module (Native ES Module) // ======================================================================= import { api } from './api.js'; import { escapeHtml, showToast, openModal, closeModal } 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'); } }); 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'; audioPlayBtn.classList.add('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 = `
${isBatchMode ? `` : ''} ${escapeHtml(c.term)} ${c.phonetic ? `${escapeHtml(c.phonetic)}` : ''} ${c.part_of_speech ? `${escapeHtml(c.part_of_speech)}` : ''}
${escapeHtml(c.translation)}
${c.context_sentence ? `
“${escapeHtml(c.context_sentence)}”
` : ''}
📁 ${escapeHtml(c.collection || 'General')} ${c.tags ? c.tags.split(',').map(t => `#${escapeHtml(t.trim())}`).join(' ') : ''}
${c.notes ? `
💡 ${escapeHtml(c.notes)}
` : ''}
`; // Edit button cardEl.querySelector('.edit-card-btn').addEventListener('click', (e) => { e.stopPropagation(); openEditModal(c); }); // Delete button cardEl.querySelector('.delete-card-btn').addEventListener('click', async (e) => { e.stopPropagation(); if (confirm(`Удалить слово "${c.term}" из словаря?`)) { await api.cards.delete(c.id); showToast(`Слово "${c.term}" удалено`, 'info'); await loadInitialData(); } }); // Batch selection click if (isBatchMode) { cardEl.addEventListener('click', () => { toggleBatchSelection(c.id); }); } cardsList.appendChild(cardEl); }); } // ----------------------------------------------------------------------- // Batch Operations // ----------------------------------------------------------------------- batchToggleBtn.addEventListener('click', () => { isBatchMode = !isBatchMode; batchToggleBtn.textContent = isBatchMode ? '✓ Завершить выбор' : '☑️ Выбрать'; batchToggleBtn.classList.toggle('btn-primary', isBatchMode); batchToggleBtn.classList.toggle('btn-outline', !isBatchMode); batchActionBar.classList.toggle('hidden', !isBatchMode); if (!isBatchMode) { selectedIds.clear(); } renderCardsList(); updateBatchCount(); }); batchCancelBtn.addEventListener('click', () => { isBatchMode = false; batchToggleBtn.textContent = '☑️ Выбрать'; batchToggleBtn.classList.remove('btn-primary'); batchToggleBtn.classList.add('btn-outline'); batchActionBar.classList.add('hidden'); selectedIds.clear(); renderCardsList(); }); function toggleBatchSelection(id) { if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id); updateBatchCount(); renderCardsList(); } batchSelectAll.addEventListener('change', () => { if (batchSelectAll.checked) { allCards.forEach(c => selectedIds.add(c.id)); } else { selectedIds.clear(); } updateBatchCount(); renderCardsList(); }); function updateBatchCount() { batchCountLabel.textContent = `Выбрано: ${selectedIds.size}`; batchSelectAll.checked = selectedIds.size > 0 && selectedIds.size === allCards.length; } batchDeleteBtn.addEventListener('click', async () => { if (selectedIds.size === 0) return; if (confirm(`Удалить выбранные карточки (${selectedIds.size} шт.)?`)) { await api.cards.batch({ card_ids: Array.from(selectedIds), action: 'delete' }); showToast(`Удалено карточек: ${selectedIds.size}`, 'info'); selectedIds.clear(); await loadInitialData(); } }); batchMoveBtn.addEventListener('click', () => { if (selectedIds.size === 0) { alert('Сначала выберите хотя бы одну карточку'); return; } openModal(moveColModal); }); batchTagBtn.addEventListener('click', async () => { if (selectedIds.size === 0) return; const tagVal = prompt('Введите тег(и) для добавления (через запятую):'); if (tagVal && tagVal.trim()) { await api.cards.batch({ card_ids: Array.from(selectedIds), action: 'add_tags', value: tagVal.trim() }); showToast(`Теги добавлены`, 'success'); await loadInitialData(); } }); // ----------------------------------------------------------------------- // Edit Card Modal // ----------------------------------------------------------------------- function openEditModal(card) { modalCardId.value = card.id; modalTerm.value = card.term; modalTranslation.value = card.translation; modalContext.value = card.context_sentence || ''; modalPhonetic.value = card.phonetic || ''; modalPos.value = card.part_of_speech || ''; modalDef.value = card.definition_en || ''; modalCollection.value = card.collection || 'General'; modalTags.value = card.tags || ''; modalNotes.value = card.notes || ''; openModal(editModal); } closeModalBtn.addEventListener('click', () => closeModal(editModal)); modalCancelBtn.addEventListener('click', () => closeModal(editModal)); modalSaveBtn.addEventListener('click', async () => { const id = modalCardId.value; const payload = { term: modalTerm.value.trim(), translation: modalTranslation.value.trim(), context_sentence: modalContext.value.trim(), phonetic: modalPhonetic.value.trim(), part_of_speech: modalPos.value.trim(), definition_en: modalDef.value.trim(), collection: modalCollection.value.trim() || 'General', tags: modalTags.value.trim(), notes: modalNotes.value.trim(), }; const resp = await api.cards.update(id, payload); if (resp.ok) { closeModal(editModal); showToast('Карточка успешно обновлена', 'success'); await loadInitialData(); } else { alert('Ошибка при сохранении карточки'); } }); // ----------------------------------------------------------------------- // Export Modal // ----------------------------------------------------------------------- if (exportOpenBtn) { exportOpenBtn.addEventListener('click', () => openModal(exportModal)); } if (drawerNavExport) { drawerNavExport.addEventListener('click', () => openModal(exportModal)); } closeExportModalBtn.addEventListener('click', () => closeModal(exportModal)); exportCancelBtn.addEventListener('click', () => closeModal(exportModal)); document.querySelectorAll('.export-tile').forEach(tile => { tile.addEventListener('click', () => { const format = tile.dataset.format; let url = `/api/export/${format}`; if (selectedCollection) url += `?collection=${encodeURIComponent(selectedCollection)}`; window.location.href = url; }); }); // ----------------------------------------------------------------------- // Collections Management // ----------------------------------------------------------------------- newCollectionBtn.addEventListener('click', () => { openModal(newColModal); newColNameInput.value = ''; newColNameInput.focus(); }); closeNewColModalBtn.addEventListener('click', () => closeModal(newColModal)); newColCancelBtn.addEventListener('click', () => closeModal(newColModal)); newColSaveBtn.addEventListener('click', () => { const name = newColNameInput.value.trim(); if (name) { selectedCollection = name; closeModal(newColModal); fetchCards(); } }); closeMoveModalBtn.addEventListener('click', () => closeModal(moveColModal)); moveColCancelBtn.addEventListener('click', () => closeModal(moveColModal)); moveColConfirmBtn.addEventListener('click', async () => { const col = moveColInput.value.trim(); if (col && selectedIds.size > 0) { await api.cards.batch({ card_ids: Array.from(selectedIds), action: 'set_collection', value: col, }); closeModal(moveColModal); selectedIds.clear(); showToast(`Перемещено в "${col}"`, 'success'); await loadInitialData(); } }); function populateDatalists() { collectionsDatalist.innerHTML = ''; const dl = document.getElementById('move-collections-datalist'); if (dl) dl.innerHTML = ''; collectionsList.forEach(c => { const opt = document.createElement('option'); opt.value = c; collectionsDatalist.appendChild(opt); if (dl) dl.appendChild(opt.cloneNode(true)); }); if (moveColQuickList) { moveColQuickList.innerHTML = ''; collectionsList.forEach(c => { const btn = document.createElement('button'); btn.className = 'btn btn-xs btn-outline'; btn.textContent = c; btn.onclick = () => { moveColInput.value = c; }; moveColQuickList.appendChild(btn); }); } } // ----------------------------------------------------------------------- // Data Loading Functions // ----------------------------------------------------------------------- async function fetchCards() { try { const params = { limit: 500 }; if (selectedCollection) params.collection = selectedCollection; if (selectedTag) params.tag = selectedTag; if (searchQuery) params.search = searchQuery; const data = await api.cards.getAll(params); allCards = data.cards || []; renderCardsList(); } catch (e) { console.error('Error fetching cards:', e); } } async function fetchCollections() { try { const cols = await api.collections.getAll(); collectionsList = cols.map(c => c.name); renderCollectionsFilter(cols); populateDatalists(); } catch (e) { console.error('Error fetching collections:', e); } } async function fetchTags() { try { const tags = await api.tags.getAll(); renderTagsFilter(tags); } catch (e) { console.error('Error fetching tags:', e); } } async function fetchStats() { try { const data = await api.stats.getStats(); if (data) { const statTotal = document.getElementById('stat-total'); const statToday = document.getElementById('stat-today'); if (statTotal) statTotal.textContent = data.total_cards; if (statToday) statToday.textContent = `+${data.today_added}`; } } catch (e) {} } async function fetchSrsStats() { try { const stats = await api.stats.getSrsStats(); if (stats) { const due = stats.due_cards || 0; const srsBadge = document.getElementById('srs-badge'); if (srsBadge) { srsBadge.textContent = due; srsBadge.style.display = due > 0 ? 'inline-block' : 'none'; } const tabSrsBadge = document.getElementById('tab-srs-badge'); if (tabSrsBadge) { tabSrsBadge.textContent = due; tabSrsBadge.classList.toggle('hidden', due === 0); } const drawerSrsBadge = document.getElementById('drawer-srs-badge'); if (drawerSrsBadge) { drawerSrsBadge.textContent = due; drawerSrsBadge.classList.toggle('hidden', due === 0); } const drawerStatDue = document.getElementById('drawer-stat-due'); if (drawerStatDue) drawerStatDue.textContent = due; } } catch (e) {} } async function loadInitialData() { await Promise.all([ fetchCards(), fetchCollections(), fetchTags(), fetchStats(), fetchSrsStats(), ]); } // Attach to module exports initDictionary.fetchCards = fetchCards; initDictionary.loadInitialData = loadInitialData; initDictionary.openEditModal = openEditModal; } export async function loadInitialData() { if (initDictionary.loadInitialData) { return await initDictionary.loadInitialData(); } } export async function fetchCards() { if (initDictionary.fetchCards) { return await initDictionary.fetchCards(); } } export function openEditModal(card) { if (initDictionary.openEditModal) { initDictionary.openEditModal(card); } }