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 = '