// --- 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); };
const installBtn = document.getElementById('installBtn');
// --- Service Worker ---
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {
// fail silently; optional log
});
});
}
// --- Install Prompt ---
let deferredInstallPrompt = null;
const isStandalone = () =>
window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
function hideInstall() {
if (installBtn) installBtn.classList.add('hidden');
}
function showInstall() {
if (installBtn) installBtn.classList.remove('hidden');
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredInstallPrompt = e;
if (!isStandalone()) showInstall();
});
window.addEventListener('appinstalled', () => {
deferredInstallPrompt = null;
hideInstall();
});
if (installBtn) {
installBtn.addEventListener('click', async () => {
if (!deferredInstallPrompt) return;
deferredInstallPrompt.prompt();
const choice = await deferredInstallPrompt.userChoice;
if (choice.outcome === 'accepted') hideInstall();
deferredInstallPrompt = null;
});
}
if (isStandalone()) hideInstall();
// --- 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 = `
`;
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; }
if (p.note === undefined) { p.note = ''; changed = true; }
p.parts.forEach(pt => {
if (pt.max === undefined) { pt.max = null; changed = true; }
if (pt.note === undefined) { pt.note = ''; 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();
}
function renameProject(pId) {
modalState = { type: 'renameProject', pId, partId: null };
const project = projects.find(p => p.id === pId);
modalTitle.innerText = "Rename Project";
modalInput.value = project.name;
modalInput.type = "text";
modalInput.placeholder = "Project name";
modal.classList.add('active');
setTimeout(() => modalInput.focus(), 100);
}
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 === 'renameProject') {
const project = projects.find(p => p.id === modalState.pId);
project.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(); });
function toggleNote(id) {
const el = document.getElementById(id);
if (!el) return;
el.classList.toggle('show');
}
function updateProjectNote(e, pId) {
const project = projects.find(p => p.id === pId);
project.note = e.target.value;
save();
}
function updatePartNote(e, pId, partId) {
const project = projects.find(p => p.id === pId);
const part = project.parts.find(pt => pt.id === partId);
part.note = e.target.value;
save();
}
// --- Render Logic ---
function render() {
app.innerHTML = '';
if (projects.length === 0) {
app.innerHTML = 'Toadstools & twine await...
Tap + to begin a new project.
';
return;
}
const grid = document.createElement('div');
grid.className = 'projects-grid';
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' : '';
const partNoteId = `part-note-${project.id}-${part.id}`;
partsHtml += `
${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}`;
const projectNoteId = `project-note-${project.id}`;
projectContainer.innerHTML = `
${partsHtml}
`;
grid.appendChild(projectContainer);
});
app.appendChild(grid);
}
render();