- Redesign top header into clean, uncluttered layout with hamburger drawer trigger, clickable logo, central SRS focus badge, theme toggle and profile avatar - Implement unified responsive Navigation Drawer for both desktop and mobile with touch swipe-to-dismiss, backdrop blur, keyboard Esc support and background scroll lock - Streamline mobile bottom tab bar to 4 core actions (Dictionary, SRS, Reader, Menu) with Menu opening the unified drawer - Synchronize real-time SRS due badges and dictionary word statistics between header, bottom tabs, and drawer - Eliminate obsolete separate mobile-menu-modal in favor of unified drawer - Bump Service Worker cache version to notes-v5.7
747 lines
30 KiB
JavaScript
747 lines
30 KiB
JavaScript
// =======================================================================
|
|
// 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 = '⭐ Обновить в словаре <span class="btn-subtext">(Ctrl + Enter)</span>';
|
|
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 = '💾 Сохранить в словарь <span class="btn-subtext">(Ctrl + Enter)</span>';
|
|
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 = '💾 Сохранить в словарь <span class="btn-subtext">(Ctrl + Enter)</span>';
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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 = `
|
|
<div class="card-top">
|
|
<div class="card-term-group">
|
|
${isBatchMode ? `<input type="checkbox" class="card-select-checkbox batch-card-check" data-id="${c.id}" ${selectedIds.has(c.id) ? 'checked' : ''}>` : ''}
|
|
<span class="dot ${srsStateClass}" title="SRS статус: ${c.srs_state || 'new'}"></span>
|
|
<span class="card-term">${escapeHtml(c.term)}</span>
|
|
${c.phonetic ? `<span class="card-phonetic">${escapeHtml(c.phonetic)}</span>` : ''}
|
|
${c.part_of_speech ? `<span class="card-pos">${escapeHtml(c.part_of_speech)}</span>` : ''}
|
|
</div>
|
|
<div class="card-actions">
|
|
<button class="icon-btn edit-card-btn" data-id="${c.id}" title="Редактировать">✏️</button>
|
|
<button class="icon-btn delete-card-btn" data-id="${c.id}" title="Удалить">🗑️</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-translation">${escapeHtml(c.translation)}</div>
|
|
${c.context_sentence ? `<div class="card-context">“${escapeHtml(c.context_sentence)}”</div>` : ''}
|
|
<div class="card-bottom">
|
|
<div class="card-tags">
|
|
<span class="card-tag">📁 ${escapeHtml(c.collection || 'General')}</span>
|
|
${c.tags ? c.tags.split(',').map(t => `<span class="card-tag">#${escapeHtml(t.trim())}</span>`).join(' ') : ''}
|
|
</div>
|
|
${c.notes ? `<div class="card-notes">💡 ${escapeHtml(c.notes)}</div>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
// 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);
|
|
}
|
|
}
|