// --- Data Init & Colors --- let projects = JSON.parse(localStorage.getItem('crochetCounters')) || []; // New Earthy/Woodland Palette extracted from image vibes const colors = [ '#b56b54', // Rust/Mushroom '#7a8b4f', // Olive Green '#cba052', // Mustard/Daisy center '#5f8a8b', // Muted Teal '#8c6246', // Warm Wood tone '#a87b8c', // Dusty Rose '#4a5d43', // Deep Forest '#9c7e63' // Taupe ]; // --- State Variables --- let modalState = { type: null, projectId: null, partId: null }; const app = document.getElementById('app'); const modal = document.getElementById('modalOverlay'); const modalInput = document.getElementById('modalInput'); const modalTitle = document.getElementById('modalTitle'); const hapticTick = () => { if ('vibrate' in navigator) navigator.vibrate(12); }; // --- Service Worker --- if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => { // fail silently; optional log }); }); } // --- Sweet-ish Alerts --- function removeSwal() { const existing = document.querySelector('.swal-overlay'); if (existing) existing.remove(); } function showConfirm({ title = 'Are you sure?', text = '', confirmText = 'Yes', cancelText = 'Cancel', danger = false } = {}) { return new Promise(resolve => { removeSwal(); const overlay = document.createElement('div'); overlay.className = 'swal-overlay'; overlay.innerHTML = `
${title}
${text}
`; const cancelBtn = overlay.querySelector('.swal-cancel'); const confirmBtn = overlay.querySelector('.swal-confirm, .swal-danger'); cancelBtn.onclick = () => { removeSwal(); resolve(false); }; confirmBtn.onclick = () => { removeSwal(); resolve(true); }; overlay.addEventListener('click', (e) => { if (e.target === overlay) { removeSwal(); resolve(false); } }); document.addEventListener('keydown', function onKey(e) { if (e.key === 'Escape') { removeSwal(); resolve(false); document.removeEventListener('keydown', onKey); } }); document.body.appendChild(overlay); }); } function showAlert({ title = 'Notice', text = '' } = {}) { return new Promise(resolve => { removeSwal(); const overlay = document.createElement('div'); overlay.className = 'swal-overlay'; overlay.innerHTML = `
${title}
${text}
`; const okBtn = overlay.querySelector('.swal-confirm'); okBtn.onclick = () => { removeSwal(); resolve(); }; overlay.addEventListener('click', (e) => { if (e.target === overlay) { removeSwal(); resolve(); } }); document.addEventListener('keydown', function onKey(e) { if (e.key === 'Escape') { removeSwal(); resolve(); document.removeEventListener('keydown', onKey); } }); document.body.appendChild(overlay); }); } // --- Theme Logic --- let isDarkMode = JSON.parse(localStorage.getItem('crochetDarkMode')); if (isDarkMode === null) { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { isDarkMode = true; } else { isDarkMode = false; } } function applyTheme() { if (isDarkMode) { document.body.classList.add('dark-mode'); document.getElementById('themeBtn').innerHTML = '🌙'; } else { document.body.classList.remove('dark-mode'); document.getElementById('themeBtn').innerHTML = '☀️'; } } function toggleTheme() { isDarkMode = !isDarkMode; localStorage.setItem('crochetDarkMode', isDarkMode); applyTheme(); } applyTheme(); // --- Focus Mode Logic --- let wakeLock = null; let isFocusMode = false; const focusBtn = document.getElementById('focusBtn'); // --- Migration Check --- if (projects.length > 0) { let changed = false; projects.forEach((p, index) => { if (!p.parts) { p.parts = []; changed = true; } if (!p.color) { p.color = colors[index % colors.length]; changed = true; } p.parts.forEach(pt => { if (pt.max === undefined) { pt.max = null; changed = true; } }); }); if (changed) { localStorage.setItem('crochetCounters', JSON.stringify(projects)); } } // --- Core Functions --- function save() { localStorage.setItem('crochetCounters', JSON.stringify(projects)); render(); } async function toggleFocusMode() { if (!isFocusMode) { try { if (document.documentElement.requestFullscreen) await document.documentElement.requestFullscreen(); if ('wakeLock' in navigator) { wakeLock = await navigator.wakeLock.request('screen'); } isFocusMode = true; focusBtn.classList.add('is-active'); } catch (err) { showAlert({ title: 'Focus Mode failed', text: err.message }); } } else { if (document.fullscreenElement) document.exitFullscreen(); if (wakeLock !== null) { wakeLock.release(); wakeLock = null; } isFocusMode = false; focusBtn.classList.remove('is-active'); } } document.addEventListener('visibilitychange', async () => { if (wakeLock !== null && document.visibilityState === 'visible') { wakeLock = await navigator.wakeLock.request('screen'); } }); // --- Interaction Logic --- async function deleteProject(pId) { const ok = await showConfirm({ title: 'Delete project?', text: 'This will remove the entire project.', confirmText: 'Delete', danger: true }); if (ok) { projects = projects.filter(p => p.id !== pId); save(); } } function toggleProjectCollapse(pId) { const project = projects.find(p => p.id === pId); project.collapsed = !project.collapsed; save(); } async function deletePart(pId, partId) { const ok = await showConfirm({ title: 'Delete part?', text: 'This part will be removed.', confirmText: 'Delete', danger: true }); if (ok) { const project = projects.find(p => p.id === pId); project.parts = project.parts.filter(pt => pt.id !== partId); save(); } } function togglePartMinimize(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.minimized = !part.minimized; save(); } function togglePartLock(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.locked = !part.locked; save(); } function togglePartFinish(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); part.finished = !part.finished; if(part.finished) part.locked = false; save(); } function updateCount(pId, partId, change) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); if (part.locked || part.finished) return; part.count += change; if (part.max !== null && part.count > part.max) part.count = part.max; if (part.count < 0) part.count = 0; hapticTick(); save(); } async function resetCount(pId, partId) { const project = projects.find(p => p.id === pId); const part = project.parts.find(pt => pt.id === partId); if (part.locked || part.finished) return; const ok = await showConfirm({ title: 'Reset count?', text: 'Set this count back to zero.', confirmText: 'Reset', danger: true }); if(ok) { part.count = 0; save(); } } // --- Modal Logic --- function openModal(type, pId = null, partId = null) { modalState = { type, pId, partId }; modalInput.value = ""; if (type === 'addProject') { modalTitle.innerText = "New Project Name"; modalInput.type = "text"; modalInput.placeholder = "e.g., Amigurumi Bear"; } else if (type === 'addPart') { modalTitle.innerText = "New Part Name"; modalInput.type = "text"; modalInput.placeholder = "e.g., Head"; } else if (type === 'setMax') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); modalTitle.innerText = "Set Max Stitches"; modalInput.value = part.max ?? ''; modalInput.type = "number"; modalInput.placeholder = "Leave blank to clear"; } else if (type === 'renamePart') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); if(part.locked || part.finished) return; modalTitle.innerText = "Rename Part"; modalInput.value = part.name; modalInput.type = "text"; } else if (type === 'manualCount') { const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId); if(part.locked || part.finished) return; modalTitle.innerText = "Set Row Count"; modalInput.value = part.count; modalInput.type = "number"; } modal.classList.add('active'); setTimeout(() => modalInput.focus(), 100); } function closeModal() { modal.classList.remove('active'); modalInput.blur(); } function saveModal() { const val = modalInput.value.trim(); if (!val && modalState.type !== 'manualCount') return closeModal(); if (modalState.type === 'addProject') { const nextColor = colors[projects.length % colors.length]; projects.push({ id: Date.now(), name: val, color: nextColor, collapsed: false, parts: [] }); projects[projects.length-1].parts.push({ id: Date.now() + 1, name: 'Part 1', count: 0, locked: false, finished: false, minimized: false, max: null }); } else if (modalState.type === 'addPart') { const project = projects.find(p => p.id === modalState.pId); project.parts.push({ id: Date.now(), name: val, count: 0, locked: false, finished: false, minimized: false, max: null }); project.collapsed = false; } else if (modalState.type === 'renamePart') { const part = projects.find(p => p.id === modalState.pId).parts.find(pt => pt.id === modalState.partId); part.name = val; } else if (modalState.type === 'manualCount') { const num = parseInt(val); if (!isNaN(num) && num >= 0) { const part = projects.find(p => p.id === modalState.pId).parts.find(pt => pt.id === modalState.partId); part.count = num; if (part.max !== null && part.count > part.max) part.count = part.max; } } else if (modalState.type === 'setMax') { const part = projects.find(p => p.id === modalState.pId).parts.find(pt => pt.id === modalState.partId); if (val === '') { part.max = null; } else { const num = parseInt(val); if (!isNaN(num) && num > 0) { part.max = num; if (part.count > part.max) part.count = part.max; } } } save(); closeModal(); } modalInput.addEventListener("keyup", (e) => { if (e.key === "Enter") saveModal(); }); // --- Render Logic --- function render() { app.innerHTML = ''; if (projects.length === 0) { app.innerHTML = '
Toadstools & twine await...
Tap + to begin a new project.
'; return; } projects.forEach(project => { const sortedParts = [...project.parts].sort((a, b) => a.finished - b.finished); const projectCollapsedClass = project.collapsed ? 'project-collapsed' : ''; let partsHtml = ''; sortedParts.forEach(part => { const isLocked = part.locked ? 'is-locked' : ''; const isFinished = part.finished ? 'is-finished' : ''; const isMinimized = part.minimized ? 'is-minimized' : ''; const lockIcon = part.locked ? '🔒' : '🔓'; const lockBtnClass = part.locked ? 'btn-lock locked-active' : 'btn-lock'; const controlsDimmed = (part.locked || part.finished) ? 'dimmed' : ''; const hideControls = part.finished ? 'hidden-controls' : ''; partsHtml += `
${part.name} ${part.count}
${part.count}
${part.max !== null ? `${part.count} / ${part.max}` : 'No max set'}
`; }); const projectContainer = document.createElement('div'); projectContainer.className = `project-container ${projectCollapsedClass}`; projectContainer.style = `--project-color: ${project.color}`; projectContainer.innerHTML = `
${project.name}
${partsHtml}
`; app.appendChild(projectContainer); }); } render();