const { ipcRenderer } = require('electron'); const Store = require('electron-store'); const store = new Store(); const DEFAULT_LISTS = [ { id: 'general', name: 'General' } ]; // Initialize data structure with migration support let checklistData = store.get('checklistData', { lists: [], selectedListId: DEFAULT_LISTS[0].id }); // Migration from single-list structure if (checklistData.items) { const migratedList = { id: 'startup', name: 'Startup', items: checklistData.items, lastCheckDate: checklistData.lastCheckDate || new Date().toDateString() }; checklistData = { lists: [migratedList], selectedListId: migratedList.id }; store.set('checklistData', checklistData); } function ensureDefaultLists() { if (!Array.isArray(checklistData.lists)) { checklistData.lists = []; } if (checklistData.lists.length === 0) { checklistData.lists.push({ id: DEFAULT_LISTS[0].id, name: DEFAULT_LISTS[0].name, items: [], lastCheckDate: new Date().toDateString() }); checklistData.selectedListId = DEFAULT_LISTS[0].id; } } ensureDefaultLists(); // DOM elements const itemTitleInput = document.getElementById('itemTitle'); const itemDescriptionInput = document.getElementById('itemDescription'); const isRecurringCheckbox = document.getElementById('isRecurring'); const recurrenceOptions = document.getElementById('recurrenceOptions'); const recurrenceTypeSelect = document.getElementById('recurrenceType'); const weeklyOptions = document.getElementById('weeklyOptions'); const monthlyOptions = document.getElementById('monthlyOptions'); const dayOfWeekSelect = document.getElementById('dayOfWeek'); const dayOfMonthInput = document.getElementById('dayOfMonth'); const addItemBtn = document.getElementById('addItemBtn'); const todoList = document.getElementById('todoList'); const doneList = document.getElementById('doneList'); const listTabs = document.getElementById('listTabs'); const addListBtn = document.getElementById('addListBtn'); const addListSelect = document.getElementById('addListSelect'); const renameListBtn = document.getElementById('renameListBtn'); const deleteListBtn = document.getElementById('deleteListBtn'); const exportJsonBtn = document.getElementById('exportJsonBtn'); const importJsonBtn = document.getElementById('importJsonBtn'); const listMenu = document.getElementById('listMenu'); const listMenuHandle = document.getElementById('listMenuHandle'); const tasksTabBtn = document.getElementById('tasksTabBtn'); const addTabBtn = document.getElementById('addTabBtn'); const tasksView = document.getElementById('tasksView'); const addView = document.getElementById('addView'); const moveItemModal = document.getElementById('moveItemModal'); const moveItemSelect = document.getElementById('moveItemSelect'); const moveItemSave = document.getElementById('moveItemSave'); const moveItemCancel = document.getElementById('moveItemCancel'); const moveItemOverlay = moveItemModal; const listModal = document.getElementById('listModal'); const listModalTitle = document.getElementById('listModalTitle'); const listModalMessage = document.getElementById('listModalMessage'); const listModalInput = document.getElementById('listModalInput'); const listModalSave = document.getElementById('listModalSave'); const listModalCancel = document.getElementById('listModalCancel'); const listModalOverlay = listModal; // alias for clarity // Edit task modal elements const editTaskModal = document.getElementById('editTaskModal'); const editTitle = document.getElementById('editTitle'); const editDescription = document.getElementById('editDescription'); const editIsRecurring = document.getElementById('editIsRecurring'); const editRecurrenceOptions = document.getElementById('editRecurrenceOptions'); const editRecurrenceType = document.getElementById('editRecurrenceType'); const editWeeklyOptions = document.getElementById('editWeeklyOptions'); const editMonthlyOptions = document.getElementById('editMonthlyOptions'); const editDayOfWeek = document.getElementById('editDayOfWeek'); const editDayOfMonth = document.getElementById('editDayOfMonth'); const editModalSave = document.getElementById('editModalSave'); const editModalCancel = document.getElementById('editModalCancel'); const editModalOverlay = editTaskModal; let addTargetListId = checklistData.selectedListId; let modalMode = null; let modalTargetId = null; let dragSourceListId = null; const FALLBACK_LIST = { id: 'new-list', name: 'New List' }; let touchDragSourceId = null; let editingTaskId = null; let moveModalItemId = null; let itemDragId = null; let itemDragDone = false; // Event listeners isRecurringCheckbox.addEventListener('change', () => { recurrenceOptions.style.display = isRecurringCheckbox.checked ? 'block' : 'none'; updateRecurrenceOptions(); }); recurrenceTypeSelect.addEventListener('change', updateRecurrenceOptions); addItemBtn.addEventListener('click', addItem); itemTitleInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addItem(); } }); addListBtn.addEventListener('click', addList); renameListBtn.addEventListener('click', renameList); deleteListBtn.addEventListener('click', deleteList); exportJsonBtn.addEventListener('click', exportChecklists); importJsonBtn.addEventListener('click', importChecklists); tasksTabBtn.addEventListener('click', () => switchTab('tasks')); addTabBtn.addEventListener('click', () => switchTab('add')); addListSelect.addEventListener('change', (e) => { addTargetListId = e.target.value; }); moveItemSave.addEventListener('click', handleMoveItemSave); moveItemCancel.addEventListener('click', closeMoveItemModal); moveItemOverlay.addEventListener('click', (e) => { if (e.target === moveItemOverlay) { closeMoveItemModal(); } }); todoList.addEventListener('dragover', (e) => onListContainerDragOver(e, false)); doneList.addEventListener('dragover', (e) => onListContainerDragOver(e, true)); todoList.addEventListener('drop', (e) => onListContainerDrop(e, false)); doneList.addEventListener('drop', (e) => onListContainerDrop(e, true)); listModalSave.addEventListener('click', handleModalSave); listModalCancel.addEventListener('click', closeListModal); listModalInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleModalSave(); } else if (e.key === 'Escape') { e.preventDefault(); closeListModal(); } }); listModalOverlay.addEventListener('click', (e) => { if (e.target === listModalOverlay) { closeListModal(); } }); // List menu (collapsible drawer for list actions) let listMenuPointerStartY = null; let listMenuPointerStartX = null; let listMenuPointerMoved = false; let listMenuSwipeHandled = false; const LIST_MENU_SWIPE_THRESHOLD = 18; const LIST_MENU_MOVE_THRESHOLD = 10; function expandListMenu() { listMenu.classList.add('expanded'); listMenuHandle.setAttribute('aria-expanded', 'true'); } function collapseListMenu() { listMenu.classList.remove('expanded'); listMenuHandle.setAttribute('aria-expanded', 'false'); } function toggleListMenu() { if (listMenu.classList.contains('expanded')) { collapseListMenu(); } else { expandListMenu(); } } listMenuHandle.addEventListener('click', () => { if (listMenuSwipeHandled) { listMenuSwipeHandled = false; return; } toggleListMenu(); }); listMenuHandle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleListMenu(); } }); listMenuHandle.addEventListener('pointerdown', (e) => { listMenuPointerStartY = e.clientY; listMenuPointerStartX = e.clientX; listMenuPointerMoved = false; try { listMenuHandle.setPointerCapture(e.pointerId); } catch (_) {} }); listMenuHandle.addEventListener('pointermove', (e) => { if (listMenuPointerStartY === null) return; const dy = e.clientY - listMenuPointerStartY; const dx = e.clientX - listMenuPointerStartX; if (Math.abs(dy) > LIST_MENU_MOVE_THRESHOLD || Math.abs(dx) > LIST_MENU_MOVE_THRESHOLD) { listMenuPointerMoved = true; } }); listMenuHandle.addEventListener('pointerup', (e) => { try { listMenuHandle.releasePointerCapture(e.pointerId); } catch (_) {} if (listMenuPointerStartY === null) { return; } const dy = e.clientY - listMenuPointerStartY; const dx = e.clientX - listMenuPointerStartX; const isSwipe = listMenuPointerMoved && Math.abs(dy) > LIST_MENU_SWIPE_THRESHOLD && Math.abs(dy) > Math.abs(dx); if (isSwipe) { if (dy > 0) { expandListMenu(); } else { collapseListMenu(); } listMenuSwipeHandled = true; } listMenuPointerStartY = null; listMenuPointerStartX = null; listMenuPointerMoved = false; }); listMenuHandle.addEventListener('pointercancel', () => { listMenuPointerStartY = null; listMenuPointerStartX = null; listMenuPointerMoved = false; }); editIsRecurring.addEventListener('change', () => { editRecurrenceOptions.style.display = editIsRecurring.checked ? 'block' : 'none'; updateEditRecurrenceOptions(); }); editRecurrenceType.addEventListener('change', updateEditRecurrenceOptions); editModalSave.addEventListener('click', saveEditTask); editModalCancel.addEventListener('click', closeEditModal); // Keep edit modal open when clicking the backdrop so text selections outside the modal don't close it. editModalOverlay.addEventListener('click', (e) => { if (e.target !== editModalOverlay) return; }); // Functions function switchTab(tab) { if (tab === 'tasks') { tasksView.classList.add('active'); addView.classList.remove('active'); tasksTabBtn.classList.add('active'); addTabBtn.classList.remove('active'); } else { tasksView.classList.remove('active'); addView.classList.add('active'); tasksTabBtn.classList.remove('active'); addTabBtn.classList.add('active'); } } function addList() { openListModal('add'); } function renameList() { openListModal('rename'); } function setSelectedList(id) { checklistData.selectedListId = id; addTargetListId = addTargetListId || id; saveData(); renderLists(); } function getCurrentList() { let list = checklistData.lists.find(l => l.id === checklistData.selectedListId); if (!list) { ensureDefaultLists(); list = checklistData.lists[0]; checklistData.selectedListId = list.id; saveData(); } return list; } function updateRecurrenceOptions() { const type = recurrenceTypeSelect.value; weeklyOptions.style.display = type === 'weekly' ? 'block' : 'none'; monthlyOptions.style.display = type === 'monthly' ? 'block' : 'none'; } function addItem() { const title = itemTitleInput.value.trim(); if (!title) { alert('Please enter a task title'); return; } const description = itemDescriptionInput.value.trim(); const isRecurring = isRecurringCheckbox.checked; let recurrence = null; if (isRecurring) { const type = recurrenceTypeSelect.value; recurrence = { type: type, dayOfWeek: type === 'weekly' ? parseInt(dayOfWeekSelect.value) : null, dayOfMonth: type === 'monthly' ? parseInt(dayOfMonthInput.value) : null }; } const newItem = { id: Date.now().toString(), title: title, description: description, completed: false, completedDate: null, recurrence: recurrence, createdAt: new Date().toISOString() }; const targetList = checklistData.lists.find(l => l.id === addTargetListId) || getCurrentList(); targetList.items.push(newItem); saveData(); renderLists(); // Reset form itemTitleInput.value = ''; itemDescriptionInput.value = ''; isRecurringCheckbox.checked = false; recurrenceOptions.style.display = 'none'; itemTitleInput.focus(); } function openEditModal(itemId) { const list = getCurrentList(); const item = list.items.find(i => i.id === itemId); if (!item) return; editingTaskId = itemId; editTitle.value = item.title; editDescription.value = item.description || ''; editIsRecurring.checked = !!item.recurrence; editRecurrenceOptions.style.display = editIsRecurring.checked ? 'block' : 'none'; if (item.recurrence) { editRecurrenceType.value = item.recurrence.type || 'daily'; editDayOfWeek.value = (item.recurrence.dayOfWeek ?? 0).toString(); editDayOfMonth.value = item.recurrence.dayOfMonth ?? 1; } else { editRecurrenceType.value = 'daily'; editDayOfWeek.value = '0'; editDayOfMonth.value = 1; } updateEditRecurrenceOptions(); editTaskModal.style.display = 'flex'; requestAnimationFrame(() => { editTitle.focus({ preventScroll: true }); editTitle.select(); }); } function closeEditModal() { editingTaskId = null; editTaskModal.style.display = 'none'; } function saveEditTask() { if (!editingTaskId) { closeEditModal(); return; } const list = getCurrentList(); const item = list.items.find(i => i.id === editingTaskId); if (!item) { closeEditModal(); return; } const title = editTitle.value.trim(); if (!title) { alert('Please enter a task title'); return; } const description = editDescription.value.trim(); let recurrence = null; if (editIsRecurring.checked) { const type = editRecurrenceType.value; recurrence = { type, dayOfWeek: type === 'weekly' ? parseInt(editDayOfWeek.value) : null, dayOfMonth: type === 'monthly' ? parseInt(editDayOfMonth.value) : null }; } item.title = title; item.description = description; item.recurrence = recurrence; saveData(); renderLists(); closeEditModal(); } function toggleItem(itemId) { const list = getCurrentList(); const item = list.items.find(i => i.id === itemId); if (!item) return; item.completed = !item.completed; item.completedDate = item.completed ? new Date().toISOString() : null; saveData(); renderLists(); } function deleteItem(itemId) { const list = getCurrentList(); if (confirm('Are you sure you want to delete this task?')) { list.items = list.items.filter(i => i.id !== itemId); saveData(); renderLists(); } } function shouldRecur(item) { if (!item.recurrence || !item.completed) return false; const now = new Date(); const completedDate = new Date(item.completedDate); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const completedDay = new Date(completedDate.getFullYear(), completedDate.getMonth(), completedDate.getDate()); switch (item.recurrence.type) { case 'daily': return completedDay.getTime() < today.getTime(); case 'weekly': const dayOfWeek = now.getDay(); return dayOfWeek === item.recurrence.dayOfWeek && completedDay.getTime() < today.getTime(); case 'monthly': const dayOfMonth = now.getDate(); return dayOfMonth === item.recurrence.dayOfMonth && completedDay.getTime() < today.getTime(); default: return false; } } function getNextOccurrenceDate(recurrence) { if (!recurrence) return null; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); switch (recurrence.type) { case 'daily': return new Date(today.getTime() + 24 * 60 * 60 * 1000); case 'weekly': const dayOfWeek = now.getDay(); const targetDay = recurrence.dayOfWeek; let daysUntil = targetDay - dayOfWeek; if (daysUntil <= 0) daysUntil += 7; return new Date(today.getTime() + daysUntil * 24 * 60 * 60 * 1000); case 'monthly': const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, recurrence.dayOfMonth); return nextMonth; default: return null; } } function formatDate(date) { if (!date) return ''; const d = new Date(date); const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; return `${days[d.getDay()]}, ${months[d.getMonth()]} ${d.getDate()}`; } function checkRecurringItems() { const today = new Date().toDateString(); checklistData.lists.forEach(list => { if (list.lastCheckDate === today) return; list.items.forEach(item => { if (shouldRecur(item)) { item.completed = false; item.completedDate = null; } }); list.lastCheckDate = today; }); saveData(); } function renderLists() { checkRecurringItems(); renderListTabs(); const list = getCurrentList(); const todoItems = list.items.filter(item => !item.completed); const doneItems = list.items.filter(item => item.completed); renderList(todoList, todoItems, false); renderList(doneList, doneItems, true); renderListSelect(); } function renderList(container, items, isDone) { if (items.length === 0) { container.innerHTML = '
No tasks yet
'; return; } container.innerHTML = items.map(item => { const recurrenceText = item.recurrence ? getRecurrenceText(item.recurrence, item.completed) : ''; return `
${escapeHtml(item.title)}
${item.description ? `
${escapeHtml(item.description)}
` : ''} ${recurrenceText ? `
🔄 ${recurrenceText}
` : ''}
`; }).join(''); } function renderListTabs() { const current = getCurrentList().id; listTabs.innerHTML = checklistData.lists.map(list => ` `).join(''); } function renderListSelect() { const current = addTargetListId || checklistData.selectedListId; addListSelect.innerHTML = checklistData.lists.map(list => ` `).join(''); if (!addTargetListId) { addTargetListId = checklistData.selectedListId; } } function openListModal(mode) { modalMode = mode; modalTargetId = getCurrentList().id; listModalTitle.textContent = mode === 'add' ? 'Add List' : 'Rename List'; listModalMessage.style.display = 'none'; listModalMessage.classList.remove('danger'); listModalInput.style.display = 'block'; listModalSave.textContent = 'Save'; listModalSave.classList.remove('btn-danger'); if (mode === 'rename') { listModalInput.value = getCurrentList().name; } else { listModalInput.value = ''; } listModal.style.display = 'flex'; requestAnimationFrame(() => { listModalInput.focus({ preventScroll: true }); listModalInput.select(); }); } function closeListModal() { modalMode = null; modalTargetId = null; listModal.style.display = 'none'; listModalInput.value = ''; } function handleModalSave() { const name = listModalInput.value.trim(); if (modalMode === 'add') { if (!name) { closeListModal(); return; } const id = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\-]/g, '') || Date.now().toString(); if (checklistData.lists.find(l => l.id === id)) { alert('A list with that name already exists.'); return; } checklistData.lists.push({ id, name, items: [], lastCheckDate: new Date().toDateString() }); checklistData.selectedListId = id; addTargetListId = id; } else if (modalMode === 'rename') { if (!name) { closeListModal(); return; } const list = checklistData.lists.find(l => l.id === modalTargetId); if (list) { list.name = name; } } else if (modalMode === 'delete') { const current = checklistData.lists.find(l => l.id === modalTargetId); if (current) { checklistData.lists = checklistData.lists.filter(l => l.id !== current.id); if (checklistData.lists.length === 0) { const newId = `${FALLBACK_LIST.id}-${Date.now()}`; const newName = `${FALLBACK_LIST.name}`; checklistData.lists.push({ id: newId, name: newName, items: [], lastCheckDate: new Date().toDateString() }); checklistData.selectedListId = newId; addTargetListId = newId; } else { if (!checklistData.lists.find(l => l.id === addTargetListId)) { addTargetListId = checklistData.lists[0].id; } checklistData.selectedListId = checklistData.lists[0].id; } } } saveData(); renderLists(); closeListModal(); } function deleteList() { modalMode = 'delete'; modalTargetId = getCurrentList().id; listModalTitle.textContent = 'Delete List'; listModalMessage.textContent = `Delete "${getCurrentList().name}"? Tasks inside will be removed. If it was the last list, a blank "New List" will be created.`; listModalMessage.style.display = 'block'; listModalMessage.classList.add('danger'); listModalInput.style.display = 'none'; listModalSave.textContent = 'Delete'; listModalSave.classList.add('btn-danger'); listModal.style.display = 'flex'; listModalInput.blur(); } function onListDragStart(id) { dragSourceListId = id; } function onListDragOver(event, id) { event.preventDefault(); const tab = findTabEl(id); if (tab) tab.classList.add('drag-over'); } function onListDragLeave(id) { const tab = findTabEl(id); if (tab) tab.classList.remove('drag-over'); } function onListDrop(event, targetId) { event.preventDefault(); const tab = findTabEl(targetId); if (tab) tab.classList.remove('drag-over'); if (!dragSourceListId || dragSourceListId === targetId) return; const lists = checklistData.lists; const fromIndex = lists.findIndex(l => l.id === dragSourceListId); const toIndex = lists.findIndex(l => l.id === targetId); if (fromIndex === -1 || toIndex === -1) return; const [moved] = lists.splice(fromIndex, 1); lists.splice(toIndex, 0, moved); saveData(); renderLists(); } function findTabEl(id) { return listTabs.querySelector(`.list-tab[onclick="changeList('${id}')"]`); } function onListPointerDown(event, id) { if (event.pointerType === 'touch') { touchDragSourceId = id; } } function onListPointerUp(event, targetId) { if (event.pointerType !== 'touch') return; if (!touchDragSourceId || touchDragSourceId === targetId) { touchDragSourceId = null; return; } const lists = checklistData.lists; const fromIndex = lists.findIndex(l => l.id === touchDragSourceId); const toIndex = lists.findIndex(l => l.id === targetId); if (fromIndex === -1 || toIndex === -1) { touchDragSourceId = null; return; } const [moved] = lists.splice(fromIndex, 1); lists.splice(toIndex, 0, moved); touchDragSourceId = null; saveData(); renderLists(); } function onListPointerCancel() { touchDragSourceId = null; } function updateEditRecurrenceOptions() { const type = editRecurrenceType.value; editWeeklyOptions.style.display = type === 'weekly' ? 'block' : 'none'; editMonthlyOptions.style.display = type === 'monthly' ? 'block' : 'none'; } function getRecurrenceText(recurrence, isCompleted) { if (isCompleted) { // Show next occurrence date for completed recurring items const nextDate = getNextOccurrenceDate(recurrence); return nextDate ? formatDate(nextDate) : ''; } else { // Show recurrence pattern for active items switch (recurrence.type) { case 'daily': return 'Repeats daily'; case 'weekly': const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return `Repeats every week on ${days[recurrence.dayOfWeek]}`; case 'monthly': return `Repeats monthly on day ${recurrence.dayOfMonth}`; default: return ''; } } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function saveData() { store.set('checklistData', checklistData); } function openMoveItemModal(itemId) { moveModalItemId = itemId; populateMoveSelect(); moveItemModal.style.display = 'flex'; requestAnimationFrame(() => { moveItemSelect.focus({ preventScroll: true }); }); } function closeMoveItemModal() { moveModalItemId = null; moveItemModal.style.display = 'none'; } function populateMoveSelect() { const options = checklistData.lists.map(list => ` `).join(''); moveItemSelect.innerHTML = options; } function handleMoveItemSave() { if (!moveModalItemId) { closeMoveItemModal(); return; } const targetListId = moveItemSelect.value; const sourceList = checklistData.lists.find(l => l.items.some(i => i.id === moveModalItemId)); const targetList = checklistData.lists.find(l => l.id === targetListId); if (!sourceList || !targetList) { closeMoveItemModal(); return; } if (sourceList.id === targetList.id) { closeMoveItemModal(); return; } const fromIndex = sourceList.items.findIndex(i => i.id === moveModalItemId); const [item] = sourceList.items.splice(fromIndex, 1); targetList.items.push(item); moveModalItemId = null; saveData(); renderLists(); closeMoveItemModal(); } function onItemDragStart(id, isDone) { itemDragId = id; itemDragDone = isDone; } function onItemDragOver(event, targetId, isDone) { event.preventDefault(); if (!itemDragId || itemDragDone !== isDone) return; const el = findItemElement(targetId); if (el) { el.classList.add('drag-over'); } } function onItemDragLeave(targetId) { const el = findItemElement(targetId); if (el) { el.classList.remove('drag-over'); } } function onItemDrop(event, targetId, isDone) { event.preventDefault(); if (!itemDragId || itemDragDone !== isDone) { clearDragState(targetId); return; } reorderItems(itemDragId, targetId); clearDragState(targetId); } function onListContainerDragOver(event, isDone) { event.preventDefault(); if (!itemDragId || itemDragDone !== isDone) return; } function onListContainerDrop(event, isDone) { event.preventDefault(); if (!itemDragId || itemDragDone !== isDone) { itemDragId = null; return; } reorderItems(itemDragId, null); itemDragId = null; itemDragDone = false; } function reorderItems(sourceId, targetId) { const list = getCurrentList(); const items = list.items; const fromIndex = items.findIndex(i => i.id === sourceId); if (fromIndex === -1) return; let toIndex = targetId ? items.findIndex(i => i.id === targetId) : items.length - 1; if (toIndex === -1) return; if (fromIndex === toIndex) return; const [moved] = items.splice(fromIndex, 1); if (fromIndex < toIndex) { toIndex -= 1; } items.splice(toIndex, 0, moved); saveData(); renderLists(); } function clearDragState(targetId) { const el = targetId ? findItemElement(targetId) : null; if (el) el.classList.remove('drag-over'); itemDragId = null; itemDragDone = false; } function findItemElement(id) { return document.querySelector(`[data-item-id="${id}"]`); } async function exportChecklists() { try { const payload = JSON.stringify(checklistData, null, 2); const result = await ipcRenderer.invoke('export-checklists', payload); if (result?.canceled) return; if (result?.error) { alert(`Failed to export checklists: ${result.error}`); return; } alert(`Checklists saved to ${result.filePath}`); } catch (error) { alert(`Failed to export checklists: ${error.message}`); } } async function importChecklists() { try { const result = await ipcRenderer.invoke('import-checklists'); if (result?.canceled) return; if (result?.error) { alert(`Failed to import checklists: ${result.error}`); return; } const parsed = parseImportedData(result?.data); if (!parsed) { alert('The selected file is not a valid checklist export.'); return; } checklistData = parsed; addTargetListId = checklistData.selectedListId; ensureDefaultLists(); saveData(); renderLists(); alert('Checklists imported successfully.'); } catch (error) { alert(`Failed to import checklists: ${error.message}`); } } function parseImportedData(rawData) { if (!rawData) return null; let parsed; try { parsed = typeof rawData === 'string' ? JSON.parse(rawData) : rawData; } catch { return null; } const lists = Array.isArray(parsed.lists) ? parsed.lists.map((list, index) => normalizeListData(list, index)).filter(Boolean) : []; if (lists.length === 0) return null; const selectedListId = lists.find(l => l.id === parsed.selectedListId)?.id || lists[0].id; return { lists, selectedListId }; } function normalizeListData(list, index) { if (!list || typeof list !== 'object') return null; const id = typeof list.id === 'string' && list.id.trim() ? list.id.trim() : `list-${Date.now()}-${index}`; const name = typeof list.name === 'string' && list.name.trim() ? list.name.trim() : 'Imported List'; const items = Array.isArray(list.items) ? list.items.map(normalizeItemData).filter(Boolean) : []; return { id, name, items, lastCheckDate: typeof list.lastCheckDate === 'string' ? list.lastCheckDate : new Date().toDateString() }; } function normalizeItemData(item) { if (!item || typeof item !== 'object') return null; const title = typeof item.title === 'string' ? item.title.trim() : ''; if (!title) return null; return { id: typeof item.id === 'string' && item.id.trim() ? item.id : `task-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, title, description: typeof item.description === 'string' ? item.description : '', completed: !!item.completed, completedDate: item.completed ? (item.completedDate || new Date().toISOString()) : null, recurrence: normalizeRecurrenceData(item.recurrence), createdAt: item.createdAt || new Date().toISOString() }; } function normalizeRecurrenceData(recurrence) { if (!recurrence || typeof recurrence !== 'object') return null; const allowedTypes = ['daily', 'weekly', 'monthly']; const type = allowedTypes.includes(recurrence.type) ? recurrence.type : 'daily'; const dayOfWeek = type === 'weekly' ? clampNumber(recurrence.dayOfWeek, 0, 6, 0) : null; const dayOfMonth = type === 'monthly' ? clampNumber(recurrence.dayOfMonth, 1, 31, 1) : null; return { type, dayOfWeek, dayOfMonth }; } function clampNumber(value, min, max, fallback) { const num = Number(value); if (Number.isInteger(num) && num >= min && num <= max) { return num; } return fallback; } // Make functions available globally for inline event handlers window.toggleItem = toggleItem; window.deleteItem = deleteItem; window.changeList = setSelectedList; window.onListDragStart = onListDragStart; window.onListDragOver = onListDragOver; window.onListDragLeave = onListDragLeave; window.onListDrop = onListDrop; window.onListPointerDown = onListPointerDown; window.onListPointerUp = onListPointerUp; window.onListPointerCancel = onListPointerCancel; window.editItem = openEditModal; window.openMoveItemModal = openMoveItemModal; window.onItemDragStart = onItemDragStart; window.onItemDragOver = onItemDragOver; window.onItemDragLeave = onItemDragLeave; window.onItemDrop = onItemDrop; // Initial render renderLists(); hideLoading(); function totalOpenItems() { return checklistData.lists.reduce((sum, list) => { return sum + list.items.filter(i => !i.completed).length; }, 0); } // Check for recurring items every minute setInterval(() => { const oldTodoCount = totalOpenItems(); checkRecurringItems(); const newTodoCount = totalOpenItems(); if (oldTodoCount !== newTodoCount) { renderLists(); } }, 60000); // Check every minute function hideLoading() { const overlay = document.getElementById('loadingOverlay'); if (overlay) { overlay.style.display = 'none'; } }