"use strict"; // Configuração e Estado Inicial const DEFAULT_CONFIG = { userName: "Explorador", city: "", bgUrl: "https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1920&q=85", bgDim: 60, accentColor: "#3b82f6", accentHover: "#2563eb", searchEngine: "google", layout: "grid", cardSize: "md", activeTab: "all", updatedAt: 0, categories: [ { id: "cat-general", name: "Geral" }, { id: "cat-media", name: "Mídia & Vídeos" }, { id: "cat-dev", name: "Dev & IA" }, { id: "cat-productivity", name: "Produtividade" } ], shortcuts: [ { id: "1", name: "YouTube", url: "https://youtube.com", desc: "Vídeos, músicas e canais", cover: "https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=600&q=80", icon: "https://www.youtube.com/s/desktop/f1e71ee6/img/favicon_144x144.png", categoryId: "cat-media" }, { id: "2", name: "GitHub", url: "https://github.com", desc: "Repositórios e projetos de código", cover: "https://images.unsplash.com/photo-1618401471353-b98afee0b2eb?w=600&q=80", icon: "https://github.githubassets.com/favicons/favicon.png", categoryId: "cat-dev" }, { id: "3", name: "ChatGPT", url: "https://chatgpt.com", desc: "Assistente de inteligência artificial", cover: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=600&q=80", icon: "https://oaistatic-cdn.azureedge.net/chatgpt/favicon.ico", categoryId: "cat-dev" }, { id: "4", name: "Reddit", url: "https://reddit.com", desc: "Comunidades e novidades do mundo", cover: "https://images.unsplash.com/photo-1542751371-adc38448a05e?w=600&q=80", icon: "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", categoryId: "cat-media" }, { id: "5", name: "Wikipedia", url: "https://pt.wikipedia.org", desc: "A enciclopédia livre", cover: "https://images.unsplash.com/photo-1457369804613-52c61a468e7d?w=600&q=80", icon: "https://pt.wikipedia.org/static/favicon/wikipedia.ico", categoryId: "cat-general" }, { id: "6", name: "Notion", url: "https://notion.so", desc: "Notas, tarefas e produtividade", cover: "https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?w=600&q=80", icon: "https://www.notion.so/images/favicon.ico", categoryId: "cat-productivity" } ] }; DEFAULT_CONFIG.tasks = []; DEFAULT_CONFIG.notes = []; const SEARCH_ENGINES = { google: { label: "Google", icon: "🌐" }, duckduckgo: { label: "DuckDuckGo", icon: "🦆" }, bing: { label: "Bing", icon: "🔎" }, brave: { label: "Brave", icon: "🦁" }, youtube: { label: "YouTube", icon: "▶️" }, github: { label: "GitHub", icon: "🐙" } }; const STORAGE_KEY = "custom_browser_homepage_data_v4"; let state = { ...DEFAULT_CONFIG }; let shortcutToDeleteId = null; let categoryToDeleteId = null; // Sincronização na Nuvem const API_BASE = "api"; let authUser = null; let syncTimer = null; let pendingCloudData = null; // Drag & Drop let draggedShortcutId = null; let draggedCategoryId = null; // Contador para IDs únicos let _idCounter = 0; function generateId() { return "id-" + Date.now().toString(36) + "-" + (++_idCounter).toString(36); } // Sanitização XSS const _escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; function escapeHtml(str) { if (!str) return ''; return String(str).replace(/[&<>"']/g, c => _escapeMap[c]); } // Validação de URL segura function sanitizeUrl(url) { if (!url || typeof url !== 'string') return ''; const trimmed = url.trim(); if (/^(javascript|data|vbscript):/i.test(trimmed)) return ''; try { const u = new URL(trimmed.startsWith('//') ? 'https:' + trimmed : trimmed); if (!['http:', 'https:'].includes(u.protocol)) return ''; return u.href; } catch { if (/^https?:\/\//i.test(trimmed)) return ''; if (/^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/i.test(trimmed)) return 'https://' + trimmed; return ''; } } // Higieniza dados vindos de fonte externa (localStorage/JSON) function sanitizeState(data) { if (!data || typeof data !== 'object') return data; const clean = { ...data }; if (Array.isArray(clean.shortcuts)) { clean.shortcuts = clean.shortcuts.map(s => ({ ...(typeof s === 'object' && s !== null ? s : {}), url: sanitizeUrl(s && s.url) || 'https://example.com', cover: sanitizeUrl(s && s.cover), icon: sanitizeUrl(s && s.icon), name: (s && s.name) || 'Sem nome', desc: (s && s.desc) || '' })); } if (Array.isArray(clean.categories)) { clean.categories = clean.categories .filter(c => c && typeof c === 'object' && typeof c.id === 'string' && c.id.trim() !== '') .map(c => ({ id: c.id, name: (c.name || 'Categoria').toString().slice(0, 60) })); } // Sanitizar tarefas if (Array.isArray(clean.tasks)) { clean.tasks = clean.tasks.map(t => { const obj = (t && typeof t === 'object') ? t : {}; return { id: (typeof obj.id === 'string' && obj.id) ? obj.id : generateId(), name: (obj.name || '').toString().slice(0, 120), desc: (obj.desc || '').toString(), date: (typeof obj.date === 'string') ? obj.date : '', priority: (['low','medium','high'].includes(obj.priority)) ? obj.priority : 'medium', checklist: Array.isArray(obj.checklist) ? obj.checklist.map(it => ({ id: (typeof it.id === 'string' && it.id) ? it.id : generateId(), text: (it.text || '').toString().slice(0, 200), done: !!it.done })) : [], createdAt: (typeof obj.createdAt === 'number') ? obj.createdAt : Date.now() }; }); } // Sanitizar notas if (Array.isArray(clean.notes)) { clean.notes = clean.notes.map(n => { const obj = (n && typeof n === 'object') ? n : {}; return { id: (typeof obj.id === 'string' && obj.id) ? obj.id : generateId(), name: (obj.name || '').toString().slice(0, 80), desc: (obj.desc || '').toString(), createdAt: (typeof obj.createdAt === 'number') ? obj.createdAt : Date.now() }; }); } if (clean.userName && typeof clean.userName === 'string') clean.userName = clean.userName.slice(0, 60); if (clean.city && typeof clean.city === 'string') clean.city = clean.city.slice(0, 60); if (typeof clean.bgUrl === 'string') { clean.bgUrl = /^https?:\/\//i.test(clean.bgUrl) ? clean.bgUrl.replace(/["'\\]/g, '') : ''; } if (typeof clean.searchEngine !== 'string' || !SEARCH_ENGINES[clean.searchEngine]) clean.searchEngine = 'google'; if (!['grid', 'dock', 'list'].includes(clean.layout)) clean.layout = 'grid'; if (!['sm', 'md', 'lg'].includes(clean.cardSize)) clean.cardSize = 'md'; if (typeof clean.updatedAt !== 'number' || !Number.isFinite(clean.updatedAt)) clean.updatedAt = 0; return clean; } // Variáveis do Calendário let calendarCurrentDate = new Date(); // Focus Trap para modais (acessibilidade) const MODAL_SELECTORS = ["#modal-shortcut", "#modal-categories", "#modal-settings", "#modal-paste-json", "#modal-delete-confirm", "#modal-delete-cat-confirm"]; function trapFocus(e) { if (e.key !== "Tab") return; const openModal = MODAL_SELECTORS .map(sel => document.querySelector(sel)) .find(m => m && !m.classList.contains("hidden")); if (!openModal) return; const focusables = openModal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } // ------------------------------------------------------------- // PERSISTÊNCIA LOCAL (LocalStorage) // ------------------------------------------------------------- function loadState() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); state = { ...DEFAULT_CONFIG, ...sanitizeState(parsed) }; if (!Array.isArray(state.categories) || state.categories.length === 0) { state.categories = [...DEFAULT_CONFIG.categories]; } } } catch (err) { console.warn("Falha ao ler dados locais:", err); } } function saveState() { try { state.updatedAt = Date.now(); localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (err) { console.warn("Falha ao salvar dados locais:", err); } try { updateTasksBadge(); } catch (e) {} } // ------------------------------------------------------------- // FEEDBACK TOAST // ------------------------------------------------------------- let toastTimeout; function showToast(msg, iconName = "info") { const toast = document.getElementById("toast"); const toastMsg = document.getElementById("toast-msg"); const toastIcon = document.getElementById("toast-icon"); toastMsg.innerText = msg; toastIcon.setAttribute("data-lucide", iconName); lucide.createIcons(); toast.classList.remove("translate-y-16", "opacity-0", "pointer-events-none"); clearTimeout(toastTimeout); toastTimeout = setTimeout(() => { toast.classList.add("translate-y-16", "opacity-0", "pointer-events-none"); }, 3000); } // ------------------------------------------------------------- // RELÓGIO E DATA INTEGRADOS // ------------------------------------------------------------- function updateClockAndGreeting() { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); document.getElementById("clock-display").innerText = `${hours}:${minutes}`; const options = { weekday: 'long', day: 'numeric', month: 'long' }; const dateFormatted = now.toLocaleDateString('pt-BR', options); document.getElementById("header-date").innerText = dateFormatted.charAt(0).toUpperCase() + dateFormatted.slice(1); const h = now.getHours(); let greeting = "Olá"; if (h >= 5 && h < 12) greeting = "Bom dia"; else if (h >= 12 && h < 18) greeting = "Boa tarde"; else if (h >= 18 && h < 24) greeting = "Boa noite"; else greeting = "Boa madrugada"; document.getElementById("greeting-text").innerText = greeting; document.getElementById("user-display-name").innerText = state.userName || "Visitante"; } // ------------------------------------------------------------- // CLIMA (Open-Meteo, gratuito e sem API key) // ------------------------------------------------------------- const WEATHER_CACHE_KEY = "initpage_weather_cache_v1"; const WEATHER_TTL = 30 * 60 * 1000; function weatherIconForCode(code) { const c = Number(code); if (c === 0) return "☀️"; if (c <= 2) return "🌤️"; if (c === 3) return "☁️"; if (c === 45 || c === 48) return "🌫️"; if (c >= 95) return "⛈️"; if (c >= 71 && c <= 77) return "❄️"; if (c >= 51 && c <= 67) return "🌦️"; if (c >= 80 && c <= 82) return "🌧️"; return "🌡️"; } function getCachedWeather(city) { try { const raw = localStorage.getItem(WEATHER_CACHE_KEY); if (!raw) return null; const cached = JSON.parse(raw); if (cached.city === city && cached.at && Date.now() - cached.at < WEATHER_TTL) { return cached; } } catch (err) { } return null; } function setCachedWeather(city, data) { try { localStorage.setItem(WEATHER_CACHE_KEY, JSON.stringify({ city, at: Date.now(), ...data })); } catch (err) { } } function renderWeather(data) { const el = document.getElementById("weather-info"); if (!data) { el.classList.add("hidden"); return; } document.getElementById("weather-icon").innerText = data.icon; document.getElementById("weather-temp").innerText = data.temp; document.getElementById("weather-city").innerText = data.city; el.classList.remove("hidden"); } async function refreshWeather() { if (!state.city || !state.city.trim()) { renderWeather(null); return; } const cityName = state.city.trim(); const cached = getCachedWeather(cityName); if (cached) { renderWeather(cached); return; } try { const geoRes = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=pt&format=json`); const geoData = await geoRes.json(); const found = geoData && geoData.results && geoData.results[0]; if (!found) { sessionStorage.setItem("initpage_weather_failed", "1"); renderWeather(null); return; } const wRes = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${found.latitude}&longitude=${found.longitude}¤t=temperature_2m,weather_code`); const wData = await wRes.json(); if (!wData || !wData.current) { renderWeather(null); return; } const temp = Math.round(wData.current.temperature_2m); const data = { icon: weatherIconForCode(wData.current.weather_code), temp: `${temp}°C`, city: (found.name || cityName) }; setCachedWeather(cityName, data); renderWeather(data); } catch (err) { renderWeather(null); } } function setupWeather() { const cached = getCachedWeather((state.city || "").trim()); if (cached) { renderWeather(cached); return; } refreshWeather(); } // ------------------------------------------------------------- // WIDGET DE CALENDÁRIO TRANSLÚCIDO // ------------------------------------------------------------- function toggleCalendarWidget() { const calWidget = document.getElementById("calendar-widget"); const isHidden = calWidget.classList.contains("hidden"); if (isHidden) { calendarCurrentDate = new Date(); renderCalendar(); calWidget.classList.remove("hidden"); } else { calWidget.classList.add("hidden"); } } function renderCalendar() { const year = calendarCurrentDate.getFullYear(); const month = calendarCurrentDate.getMonth(); const monthName = new Intl.DateTimeFormat('pt-BR', { month: 'long', year: 'numeric' }).format(calendarCurrentDate); document.getElementById("cal-month-year").innerText = monthName; const firstDayIndex = new Date(year, month, 1).getDay(); const totalDaysInMonth = new Date(year, month + 1, 0).getDate(); const totalDaysInPrevMonth = new Date(year, month, 0).getDate(); const grid = document.getElementById("cal-days-grid"); grid.innerHTML = ""; const today = new Date(); const isCurrentMonthAndYear = (today.getFullYear() === year && today.getMonth() === month); for (let i = firstDayIndex - 1; i >= 0; i--) { const dayNum = totalDaysInPrevMonth - i; const cell = document.createElement("div"); cell.className = "py-1.5 text-slate-500 cursor-default opacity-50 cal-day-cell"; cell.innerText = dayNum; grid.appendChild(cell); } window._calendarSelectedDay = window._calendarSelectedDay || today.toISOString().slice(0,10); for (let day = 1; day <= totalDaysInMonth; day++) { const cell = document.createElement("button"); cell.type = "button"; const thisDate = new Date(year, month, day); const dateStr = thisDate.toISOString().slice(0,10); const isToday = isCurrentMonthAndYear && (day === today.getDate()); const hasTasks = getTasksForDate(dateStr).length > 0; const isSelected = window._calendarSelectedDay === dateStr; cell.className = `cal-day-cell py-1.5 rounded-xl transition-all duration-150 font-medium relative ${ isSelected ? "bg-accent/80 text-white font-bold ring-2 ring-white/40" : isToday ? "bg-accent text-white font-bold shadow-md ring-2 ring-white/30" : "text-slate-200 hover:bg-white/15 hover:text-white" }`; cell.innerHTML = `${day}${hasTasks ? '' : ''}`; cell.dataset.dateStr = dateStr; cell.onclick = () => { window._calendarSelectedDay = dateStr; const clickedDate = new Date(year, month, day); const dateStrPt = clickedDate.toLocaleDateString('pt-BR', { weekday: 'short', day: 'numeric', month: 'short' }); document.getElementById("cal-selected-info").innerText = isToday ? "Hoje" : dateStrPt; renderCalendar(); renderCalendarDayTasks(); }; grid.appendChild(cell); } const totalRendered = firstDayIndex + totalDaysInMonth; const remainingDays = (7 - (totalRendered % 7)) % 7; for (let day = 1; day <= remainingDays; day++) { const cell = document.createElement("div"); cell.className = "py-1.5 text-slate-500 cursor-default opacity-50 cal-day-cell"; cell.innerText = day; grid.appendChild(cell); } renderCalendarDayTasks(); } function changeCalendarMonth(delta) { calendarCurrentDate.setMonth(calendarCurrentDate.getMonth() + delta); renderCalendar(); } // ------------------------------------------------------------- // TEMA E CORES // ------------------------------------------------------------- function applyTheme() { document.documentElement.style.setProperty('--accent-color', state.accentColor); document.documentElement.style.setProperty('--accent-hover', state.accentHover); const bgCover = document.getElementById("bg-cover"); bgCover.style.backgroundImage = state.bgUrl ? `url("${state.bgUrl.replace(/["'\\]/g, '')}")` : 'none'; const dim = state.bgDim || 60; document.getElementById("bg-overlay").style.backgroundColor = `rgba(0, 0, 0, ${dim / 100})`; document.getElementById("val-bg-dim").innerText = `${dim}%`; document.getElementById("setting-user-name").value = state.userName; document.getElementById("setting-city").value = state.city || ""; document.getElementById("setting-bg-url").value = state.bgUrl; document.getElementById("setting-bg-dim").value = state.bgDim; document.getElementById("setting-custom-color").value = state.accentColor; document.querySelectorAll(".layout-btn").forEach(btn => { btn.classList.toggle("selected", btn.dataset.layout === state.layout); }); document.querySelectorAll(".size-btn").forEach(btn => { btn.classList.toggle("selected", btn.dataset.size === state.cardSize); }); updateSearchEngineUI(); } function updateSearchEngineUI() { const eng = SEARCH_ENGINES[state.searchEngine] || SEARCH_ENGINES.google; document.getElementById("search-engine-icon").innerText = eng.icon; document.getElementById("search-engine-label").innerText = eng.label; document.getElementById("search-input").placeholder = `Buscar no ${eng.label} ou digitar URL...`; } // ------------------------------------------------------------- // GESTÃO DE ABAS COM BOTÃO '+' AO FINAL // ------------------------------------------------------------- function renderTabs() { const wrapper = document.getElementById("tabs-wrapper"); if (!wrapper) return; // Não re-renderiza abas durante busca const searchInput = document.getElementById("search-input"); if (searchInput && searchInput.value && searchInput.value.trim()) { return; } wrapper.innerHTML = ""; const btnAll = createTabButton("all", "Todos", state.activeTab === "all", false); wrapper.appendChild(btnAll); state.categories.forEach(cat => { const btnCat = createTabButton(cat.id, cat.name, state.activeTab === cat.id, true); wrapper.appendChild(btnCat); }); const btnAddCat = document.createElement("button"); btnAddCat.type = "button"; btnAddCat.title = "Gerenciar ou adicionar categorias"; btnAddCat.className = "flex items-center justify-center w-7 h-7 rounded-xl glass-card text-slate-300 hover:text-white hover:bg-white/20 transition active:scale-95 flex-shrink-0 ml-1"; btnAddCat.innerHTML = ''; btnAddCat.onclick = openCategoriesModal; wrapper.appendChild(btnAddCat); lucide.createIcons(); } function createTabButton(id, label, isActive, isDraggable) { const tab = document.createElement("button"); tab.dataset.tabId = id; tab.className = `tab-btn flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-xs font-semibold whitespace-nowrap transition-all duration-200 select-none ${ isActive ? "selected" : "glass-card text-slate-300 hover:text-white hover:bg-white/15" }`; tab.innerHTML = `${escapeHtml(label)}`; tab.onclick = () => switchTab(id); tab.addEventListener("dragover", (e) => { if (draggedShortcutId && id !== "all") { e.preventDefault(); tab.classList.add("tab-drag-over"); } else if (draggedCategoryId && isDraggable) { e.preventDefault(); tab.classList.add("tab-drag-over"); } }); tab.addEventListener("dragleave", () => { tab.classList.remove("tab-drag-over"); }); tab.addEventListener("drop", (e) => { tab.classList.remove("tab-drag-over"); if (draggedShortcutId && id !== "all") { e.preventDefault(); const shortcut = state.shortcuts.find(s => s.id === draggedShortcutId); if (shortcut) { shortcut.categoryId = id; saveState(); renderShortcuts(); populateCategorySelect(); showToast(`Atalho movido para "${label}"`, "folder"); scheduleSync(); } return; } if (draggedCategoryId && isDraggable && draggedCategoryId !== id) { e.preventDefault(); const oldIndex = state.categories.findIndex(c => c.id === draggedCategoryId); const targetIndex = state.categories.findIndex(c => c.id === id); if (oldIndex !== -1 && targetIndex !== -1) { const [movedCat] = state.categories.splice(oldIndex, 1); state.categories.splice(targetIndex, 0, movedCat); saveState(); renderTabs(); showToast("Ordem das categorias atualizada!", "move"); } } }); if (isDraggable) { tab.draggable = true; tab.addEventListener("dragstart", (e) => { draggedCategoryId = id; e.dataTransfer.setData("text/category-id", id); tab.classList.add("opacity-50"); }); tab.addEventListener("dragend", () => { draggedCategoryId = null; tab.classList.remove("opacity-50"); }); } return tab; } function switchTab(tabId) { if (state.activeTab === tabId) return; state.activeTab = tabId; const searchInput = document.getElementById("search-input"); if (searchInput) searchInput.value = ""; const container = document.getElementById("shortcuts-container"); container.classList.add("switching"); renderTabs(); setTimeout(() => { renderShortcuts(); container.classList.remove("switching"); }, 140); } // ------------------------------------------------------------- // RENDERIZAÇÃO DOS ATALHOS + CARD '+' AO FINAL // ------------------------------------------------------------- function getFilteredShortcuts() { if (state.activeTab === "all") { return [...state.shortcuts]; } return state.shortcuts.filter(s => s.categoryId === state.activeTab); } function renderShortcuts(searchQuery = "") { const container = document.getElementById("shortcuts-container"); const tabsWrapper = document.getElementById("tabs-wrapper"); if (!container) return; container.innerHTML = ""; const query = (searchQuery || "").trim().toLowerCase(); let list; if (query) { list = state.shortcuts.filter(s => { const name = (s.name || "").toLowerCase(); const desc = (s.desc || "").toLowerCase(); const url = (s.url || "").toLowerCase(); return name.includes(query) || desc.includes(query) || url.includes(query); }); // Ocultar abas durante a busca if (tabsWrapper) { tabsWrapper.classList.add("opacity-40", "pointer-events-none"); } } else { list = getFilteredShortcuts(); if (tabsWrapper) { tabsWrapper.classList.remove("opacity-40", "pointer-events-none"); } } document.getElementById("shortcuts-count").innerText = `${list.length} ${list.length === 1 ? 'atalho' : 'atalhos'}`; if (state.layout === "grid") { container.className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3.5 sm:gap-4"; } else if (state.layout === "dock") { container.className = "flex flex-wrap items-center justify-center gap-3 py-3 px-5 rounded-3xl glass-panel max-w-fit mx-auto"; } else if (state.layout === "list") { container.className = "flex flex-col gap-2.5 max-w-3xl mx-auto"; } list.forEach(item => { const card = createShortcutCard(item); container.appendChild(card); }); const addCard = createAddShortcutCard(); window.dispatchEvent(new CustomEvent("initpage:shortcuts-rendered", { detail: { activeTabId: state.activeTab, activeTabName: state.activeTab === "all" ? null : ((state.categories.find(c => c.id === state.activeTab) || {}).name || null), layout: state.layout, cardSize: state.cardSize } })); container.appendChild(addCard); lucide.createIcons(); } function createAddShortcutCard() { const card = document.createElement("div"); card.className = "group relative glass-card border-dashed border-white/20 hover:border-accent hover:bg-white/10 rounded-2xl overflow-hidden cursor-pointer select-none flex flex-col items-center justify-center transition-all duration-300"; if (state.layout === "grid") { let bannerHeight = "h-24 sm:h-28"; if (state.cardSize === "sm") bannerHeight = "h-16 sm:h-20"; if (state.cardSize === "lg") bannerHeight = "h-32 sm:h-36"; card.innerHTML = `

Adicionar Atalho

`; } else if (state.layout === "dock") { card.classList.add("p-2.5", "flex", "flex-col", "items-center", "w-20"); card.innerHTML = `
Adicionar `; } else if (state.layout === "list") { card.classList.add("flex", "items-center", "p-4", "gap-4", "justify-center"); card.innerHTML = `
Adicionar novo atalho nesta categoria `; } card.onclick = () => openShortcutModal(); return card; } function createShortcutCard(item) { const card = document.createElement("div"); card.dataset.id = item.id; card.draggable = true; card.className = "group relative glass-card rounded-2xl overflow-hidden cursor-pointer select-none"; const safeIcon = sanitizeUrl(item.icon) || `https://www.google.com/s2/favicons?domain=${encodeURIComponent(item.url)}&sz=128`; const safeCover = sanitizeUrl(item.cover); const safeName = escapeHtml(item.name); const safeDesc = escapeHtml(item.desc || item.url.replace(/^https?:\/\//, '')); const hasCover = !!safeCover; const coverHtml = hasCover ? `${safeName}` : `
`; const categoryObj = state.categories.find(c => c.id === item.categoryId); const categoryName = escapeHtml(categoryObj ? categoryObj.name : "Geral"); if (state.layout === "grid") { let bannerHeight = "h-24 sm:h-28"; if (state.cardSize === "sm") bannerHeight = "h-16 sm:h-20"; if (state.cardSize === "lg") bannerHeight = "h-32 sm:h-36"; card.innerHTML = `
${coverHtml}
${categoryName}

${safeName}

${safeDesc}

`; } else if (state.layout === "dock") { card.classList.add("p-2.5", "flex", "flex-col", "items-center", "w-20"); card.innerHTML = `
${safeName} `; } else if (state.layout === "list") { card.classList.add("flex", "items-center", "p-3", "gap-4"); card.innerHTML = `
${coverHtml}

${safeName}

${categoryName}

${safeDesc}

`; } const actions = document.createElement("div"); actions.className = "card-actions absolute top-2 right-2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity bg-slate-950/80 rounded-lg p-1 border border-white/15 shadow-xl z-20"; const btnEdit = document.createElement("button"); btnEdit.className = "p-1 rounded hover:bg-white/20 text-slate-300 hover:text-white transition"; btnEdit.title = "Editar"; btnEdit.setAttribute("aria-label", `Editar atalho ${safeName}`); btnEdit.innerHTML = ''; btnEdit.onclick = (e) => { e.stopPropagation(); openShortcutModal(item); }; const btnDelete = document.createElement("button"); btnDelete.className = "p-1 rounded hover:bg-rose-500/20 text-slate-400 hover:text-rose-400 transition"; btnDelete.title = "Excluir"; btnDelete.setAttribute("aria-label", `Excluir atalho ${safeName}`); btnDelete.innerHTML = ''; btnDelete.onclick = (e) => { e.stopPropagation(); askDeleteShortcut(item.id, item.name); }; actions.appendChild(btnEdit); actions.appendChild(btnDelete); card.appendChild(actions); card.onclick = (e) => { if (e.target.closest("button")) return; const target = sanitizeUrl(item.url); if (target) window.open(target, "_blank", "noopener,noreferrer"); }; card.addEventListener("dragstart", (e) => { draggedShortcutId = item.id; e.dataTransfer.setData("text/shortcut-id", item.id); card.classList.add("card-dragging"); }); card.addEventListener("dragend", () => { draggedShortcutId = null; card.classList.remove("card-dragging"); document.querySelectorAll(".card-drag-over").forEach(el => el.classList.remove("card-drag-over")); }); card.addEventListener("dragover", (e) => { if (draggedShortcutId && draggedShortcutId !== item.id) { e.preventDefault(); card.classList.add("card-drag-over"); } }); card.addEventListener("dragleave", () => { card.classList.remove("card-drag-over"); }); card.addEventListener("drop", (e) => { e.preventDefault(); card.classList.remove("card-drag-over"); if (draggedShortcutId && draggedShortcutId !== item.id) { const oldIndex = state.shortcuts.findIndex(s => s.id === draggedShortcutId); const targetIndex = state.shortcuts.findIndex(s => s.id === item.id); if (oldIndex !== -1 && targetIndex !== -1) { const [movedItem] = state.shortcuts.splice(oldIndex, 1); state.shortcuts.splice(targetIndex, 0, movedItem); saveState(); renderShortcuts(); showToast("Atalho reposicionado!", "move"); } } }); return card; } // ------------------------------------------------------------- // GERENCIAMENTO (CRUD) DE CATEGORIAS // ------------------------------------------------------------- function openCategoriesModal() { renderCategoriesCrudList(); resetCategoryForm(); document.getElementById("modal-categories").classList.remove("hidden"); } function closeCategoriesModal() { document.getElementById("modal-categories").classList.add("hidden"); resetCategoryForm(); } function resetCategoryForm() { document.getElementById("input-cat-id").value = ""; document.getElementById("input-cat-name").value = ""; document.getElementById("btn-submit-cat-text").innerText = "Adicionar"; document.getElementById("btn-cancel-cat-edit").classList.add("hidden"); } function renderCategoriesCrudList() { const container = document.getElementById("categories-crud-list"); container.innerHTML = ""; state.categories.forEach(cat => { const count = state.shortcuts.filter(s => s.categoryId === cat.id).length; const row = document.createElement("div"); row.className = "flex items-center justify-between p-2 rounded-xl glass-panel text-xs hover:bg-white/10 transition"; row.innerHTML = `
${escapeHtml(cat.name)} (${count} atalhos)
`; row.querySelector(".btn-edit-cat").onclick = () => { document.getElementById("input-cat-id").value = cat.id; document.getElementById("input-cat-name").value = cat.name; document.getElementById("btn-submit-cat-text").innerText = "Atualizar"; document.getElementById("btn-cancel-cat-edit").classList.remove("hidden"); document.getElementById("input-cat-name").focus(); }; row.querySelector(".btn-del-cat").onclick = () => { askDeleteCategory(cat.id, cat.name); }; container.appendChild(row); }); lucide.createIcons(); } function handleCategoryFormSubmit(e) { e.preventDefault(); const id = document.getElementById("input-cat-id").value; const name = document.getElementById("input-cat-name").value.trim(); if (!name) return; if (id) { state.categories = state.categories.map(c => c.id === id ? { ...c, name } : c); showToast(`Categoria "${name}" atualizada!`, "check"); scheduleSync(); } else { const newCat = { id: generateId(), name }; state.categories.push(newCat); showToast(`Categoria "${name}" criada!`, "plus"); scheduleSync(); } saveState(); resetCategoryForm(); renderCategoriesCrudList(); renderTabs(); populateCategorySelect(); } function askDeleteCategory(id, name) { if (state.categories.length <= 1) { showToast("Você precisa manter pelo menos uma categoria!", "alert-circle"); return; } categoryToDeleteId = id; document.getElementById("delete-cat-confirm-text").innerText = `Deseja excluir a categoria "${name}"? Os atalhos associados a ela serão transferidos para outra categoria.`; document.getElementById("modal-delete-cat-confirm").classList.remove("hidden"); } function confirmDeleteCategory() { if (!categoryToDeleteId) return; const fallbackCat = state.categories.find(c => c.id !== categoryToDeleteId); const fallbackId = fallbackCat ? fallbackCat.id : "cat-general"; state.shortcuts = state.shortcuts.map(s => { if (s.categoryId === categoryToDeleteId) { return { ...s, categoryId: fallbackId }; } return s; }); if (state.activeTab === categoryToDeleteId) { state.activeTab = "all"; } state.categories = state.categories.filter(c => c.id !== categoryToDeleteId); categoryToDeleteId = null; saveState(); document.getElementById("modal-delete-cat-confirm").classList.add("hidden"); renderCategoriesCrudList(); renderTabs(); renderShortcuts(); populateCategorySelect(); showToast("Categoria excluída!", "trash-2"); scheduleSync(); } function populateCategorySelect(selectedId = null) { const select = document.getElementById("input-shortcut-category"); if (!select) return; const prevVal = selectedId || select.value; select.innerHTML = ""; state.categories.forEach(cat => { const opt = document.createElement("option"); opt.value = cat.id; opt.innerText = cat.name; if (prevVal && prevVal === cat.id) { opt.selected = true; } select.appendChild(opt); }); } // ------------------------------------------------------------- // MODAL DE ATALHOS // ------------------------------------------------------------- function openShortcutModal(shortcut = null) { const modal = document.getElementById("modal-shortcut"); const title = document.getElementById("modal-shortcut-title"); const inputId = document.getElementById("input-shortcut-id"); const inputName = document.getElementById("input-shortcut-name"); const inputUrl = document.getElementById("input-shortcut-url"); const inputCover = document.getElementById("input-shortcut-cover"); const inputDesc = document.getElementById("input-shortcut-desc"); const inputIcon = document.getElementById("input-shortcut-icon"); const targetCatId = shortcut ? shortcut.categoryId : (state.activeTab !== "all" ? state.activeTab : null); populateCategorySelect(targetCatId); if (shortcut) { title.innerHTML = ' Editar Atalho'; inputId.value = shortcut.id; inputName.value = shortcut.name; inputUrl.value = shortcut.url; inputCover.value = shortcut.cover || ""; inputDesc.value = shortcut.desc || ""; inputIcon.value = shortcut.icon || ""; } else { title.innerHTML = ' Novo Atalho'; inputId.value = ""; inputName.value = ""; inputUrl.value = ""; inputCover.value = ""; inputDesc.value = ""; inputIcon.value = ""; } modal.classList.remove("hidden"); lucide.createIcons(); setTimeout(() => inputName.focus(), 60); } function closeShortcutModal() { document.getElementById("modal-shortcut").classList.add("hidden"); } function handleShortcutFormSubmit(e) { e.preventDefault(); const id = document.getElementById("input-shortcut-id").value; const name = document.getElementById("input-shortcut-name").value.trim(); let url = document.getElementById("input-shortcut-url").value.trim(); const categoryId = document.getElementById("input-shortcut-category").value; const cover = document.getElementById("input-shortcut-cover").value.trim(); const desc = document.getElementById("input-shortcut-desc").value.trim(); const icon = document.getElementById("input-shortcut-icon").value.trim(); const safeUrl = sanitizeUrl(url); if (!safeUrl) { showToast("Endereço (URL) inválido. Use http:// ou https://", "alert-triangle"); return; } url = safeUrl; const safeCover = sanitizeUrl(cover); const safeIcon = sanitizeUrl(icon); if (id) { state.shortcuts = state.shortcuts.map(s => s.id === id ? { ...s, name, url, categoryId, cover: safeCover, desc, icon: safeIcon } : s); showToast("Atalho atualizado!", "check"); } else { const newShortcut = { id: generateId(), name, url, categoryId, cover: safeCover, desc, icon: safeIcon }; state.shortcuts.push(newShortcut); showToast("Novo atalho adicionado!", "bookmark-plus"); } saveState(); renderShortcuts(); closeShortcutModal(); showToast("Atalho adicionado!", "bookmark-plus"); scheduleSync(); } function askDeleteShortcut(id, name) { shortcutToDeleteId = id; document.getElementById("delete-confirm-text").innerText = `Deseja realmente remover o atalho "${name}"?`; document.getElementById("modal-delete-confirm").classList.remove("hidden"); } function closeDeleteModal() { shortcutToDeleteId = null; document.getElementById("modal-delete-confirm").classList.add("hidden"); } function confirmDeleteShortcut() { if (!shortcutToDeleteId) return; state.shortcuts = state.shortcuts.filter(s => s.id !== shortcutToDeleteId); saveState(); renderShortcuts(); closeDeleteModal(); showToast("Atalho removido!", "trash-2"); scheduleSync(); } // ------------------------------------------------------------- // BUSCADOR WEB // ------------------------------------------------------------- function buildSearchUrl(engine, rawQuery) { const term = rawQuery.trim(); if (!term) return null; const isDirectDomain = /^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/i.test(term); const hasProtocol = /^https?:\/\//i.test(term); if (hasProtocol) return sanitizeUrl(term) || null; if (isDirectDomain && !term.includes(" ")) return sanitizeUrl('https://' + term) || null; const encoded = encodeURIComponent(term); switch (engine) { case "google": return `https://www.google.com/search?q=${encoded}`; case "duckduckgo": return `https://duckduckgo.com/?q=${encoded}`; case "bing": return `https://www.bing.com/search?q=${encoded}`; case "brave": return `https://search.brave.com/search?q=${encoded}`; case "youtube": return `https://www.youtube.com/results?search_query=${encoded}`; case "github": return `https://github.com/search?q=${encoded}&type=repositories`; default: return `https://www.google.com/search?q=${encoded}`; } } function handleSearchSubmit(e) { if (e) e.preventDefault(); const input = document.getElementById("search-input"); const query = (input.value || "").trim(); if (!query) return; // Se houver resultado exato de atalho? Mantemos comportamento padrão: busca na web const targetUrl = buildSearchUrl(state.searchEngine, query); if (targetUrl) { window.open(targetUrl, "_blank", "noopener,noreferrer"); } } // ------------------------------------------------------------- // CONFIGURAÇÕES E EXPORTAÇÃO // ------------------------------------------------------------- function openSettingsModal() { document.getElementById("modal-settings").classList.remove("hidden"); } function closeSettingsModal() { document.getElementById("modal-settings").classList.add("hidden"); } function exportConfiguration() { const jsonStr = JSON.stringify(state, null, 2); const fileName = `minha-pagina-inicial-${new Date().toISOString().slice(0, 10)}.json`; const file = new File([jsonStr], fileName, { type: "application/json" }); if (navigator.canShare && navigator.share && navigator.canShare({ files: [file] })) { navigator.share({ files: [file], title: "Configurações da página inicial" }) .then(() => showToast("Configuração exportada com sucesso!", "download")) .catch(() => {}); return; } if (/ArcMobile2|Arc Search/i.test(navigator.userAgent)) { copyConfigurationToClipboard(); return; } exportConfigurationFallback(file); } function exportConfigurationFallback(file) { const url = URL.createObjectURL(file); const downloadAnchor = document.createElement("a"); downloadAnchor.href = url; downloadAnchor.download = file.name; document.body.appendChild(downloadAnchor); downloadAnchor.click(); document.body.removeChild(downloadAnchor); setTimeout(() => URL.revokeObjectURL(url), 60000); showToast("Configuração exportada com sucesso!", "download"); } function copyConfigurationToClipboard() { const jsonStr = JSON.stringify(state, null, 2); const textarea = document.createElement("textarea"); textarea.value = jsonStr; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.top = "0"; textarea.style.left = "0"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); textarea.setSelectionRange(0, 99999); let ok = false; try { ok = document.execCommand("copy"); } catch (err) { ok = false; } document.body.removeChild(textarea); if (ok) { showToast("JSON copiado! Cole em um arquivo .json", "copy"); } else if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(jsonStr) .then(() => showToast("JSON copiado! Cole em um arquivo .json", "copy")) .catch(() => showToast("Não foi possível copiar.", "alert-triangle")); } else { showToast("Não foi possível copiar.", "alert-triangle"); } } function importConfiguration(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { applyImportedConfiguration(e.target.result); }; reader.readAsText(file); event.target.value = ""; } function applyImportedConfiguration(rawText) { try { const imported = JSON.parse(rawText); if (imported && Array.isArray(imported.shortcuts)) { state = { ...DEFAULT_CONFIG, ...sanitizeState(imported) }; if (!Array.isArray(state.categories) || state.categories.length === 0) { state.categories = [...DEFAULT_CONFIG.categories]; } saveState(); applyAllSettings(); showToast("Configuração importada com sucesso!", "check"); scheduleSync(); } else { showToast("JSON inválido!", "alert-triangle"); } } catch (err) { showToast("Erro ao ler JSON.", "alert-triangle"); } } function openPasteJsonModal() { document.getElementById("input-paste-json").value = ""; document.getElementById("modal-paste-json").classList.remove("hidden"); setTimeout(() => document.getElementById("input-paste-json").focus(), 60); } function closePasteJsonModal() { document.getElementById("modal-paste-json").classList.add("hidden"); } function confirmPasteJson() { const text = document.getElementById("input-paste-json").value.trim(); if (!text) { showToast("Cole o JSON no campo primeiro.", "alert-triangle"); return; } applyImportedConfiguration(text); closePasteJsonModal(); } function pasteJsonFromClipboard() { if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText() .then(text => { if (!text || !text.trim()) { openPasteJsonModal(); return; } applyImportedConfiguration(text); }) .catch(() => openPasteJsonModal()); } else { openPasteJsonModal(); } } function applyAllSettings() { applyTheme(); renderTabs(); renderShortcuts(); updateClockAndGreeting(); } // ------------------------------------------------------------- // EVENT LISTENERS GERAIS // ------------------------------------------------------------- window.addEventListener("DOMContentLoaded", () => { loadState(); applyAllSettings(); setupWeather(); setInterval(updateClockAndGreeting, 1000); setInterval(refreshWeather, 30 * 60 * 1000); // Service Worker (PWA / offline) if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').catch(() => {}); } // Fechar modais ao clicar fora (backdrop) const modalClosers = { "modal-shortcut": closeShortcutModal, "modal-categories": closeCategoriesModal, "modal-settings": closeSettingsModal, "modal-paste-json": closePasteJsonModal, "modal-delete-confirm": closeDeleteModal, "modal-delete-cat-confirm": () => { categoryToDeleteId = null; document.getElementById("modal-delete-cat-confirm").classList.add("hidden"); } }; Object.entries(modalClosers).forEach(([id, closer]) => { const el = document.getElementById(id); el.addEventListener("click", (e) => { if (e.target === el) closer(); }); }); // Atalhos de teclado window.addEventListener("keydown", (e) => { trapFocus(e); if ((e.key === "/" || (e.ctrlKey && e.key.toLowerCase() === "k")) && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") { e.preventDefault(); document.getElementById("search-input").focus(); } if (e.key === "Escape") { closeShortcutModal(); closeSettingsModal(); closeDeleteModal(); closeCategoriesModal(); closePasteJsonModal(); document.getElementById("modal-delete-cat-confirm").classList.add("hidden"); document.getElementById("search-engine-menu").classList.add("hidden"); document.getElementById("calendar-widget").classList.add("hidden"); } }); // Busca document.getElementById("search-form").addEventListener("submit", handleSearchSubmit); const searchInput = document.getElementById("search-input"); if (searchInput) { searchInput.addEventListener("input", () => { renderShortcuts(searchInput.value); }); } // Menu de Buscadores const searchEngineBtn = document.getElementById("search-engine-btn"); const searchEngineMenu = document.getElementById("search-engine-menu"); searchEngineBtn.addEventListener("click", (e) => { e.stopPropagation(); searchEngineMenu.classList.toggle("hidden"); }); document.addEventListener("click", () => { searchEngineMenu.classList.add("hidden"); }); document.querySelectorAll(".engine-opt").forEach(btn => { btn.addEventListener("click", () => { state.searchEngine = btn.dataset.engine; saveState(); updateSearchEngineUI(); searchEngineMenu.classList.add("hidden"); showToast(`Buscador: ${SEARCH_ENGINES[state.searchEngine].label}`, "search"); }); }); // Widget de Calendário Integrado ao Relógio / Saudação const clockTrigger = document.getElementById("clock-calendar-trigger"); clockTrigger.addEventListener("click", (e) => { if (e.target.id === "user-display-name") { e.stopPropagation(); openSettingsModal(); document.getElementById("setting-user-name").focus(); return; } e.stopPropagation(); toggleCalendarWidget(); }); // Navegação do Calendário document.getElementById("cal-prev-month").addEventListener("click", (e) => { e.stopPropagation(); changeCalendarMonth(-1); }); document.getElementById("cal-next-month").addEventListener("click", (e) => { e.stopPropagation(); changeCalendarMonth(1); }); document.getElementById("cal-btn-today").addEventListener("click", (e) => { e.stopPropagation(); calendarCurrentDate = new Date(); renderCalendar(); }); document.getElementById("calendar-widget").addEventListener("click", (e) => { e.stopPropagation(); }); // Categorias document.getElementById("btn-close-categories-modal").addEventListener("click", closeCategoriesModal); document.getElementById("btn-done-categories").addEventListener("click", closeCategoriesModal); document.getElementById("form-category").addEventListener("submit", handleCategoryFormSubmit); document.getElementById("btn-cancel-cat-edit").addEventListener("click", resetCategoryForm); document.getElementById("btn-cancel-cat-delete").addEventListener("click", () => { document.getElementById("modal-delete-cat-confirm").classList.add("hidden"); }); document.getElementById("btn-confirm-cat-delete").addEventListener("click", confirmDeleteCategory); document.getElementById("btn-shortcut-new-cat").addEventListener("click", () => { openCategoriesModal(); }); // Atalhos document.getElementById("btn-close-shortcut-modal").addEventListener("click", closeShortcutModal); document.getElementById("btn-cancel-shortcut").addEventListener("click", closeShortcutModal); document.getElementById("form-shortcut").addEventListener("submit", handleShortcutFormSubmit); document.getElementById("btn-cancel-delete").addEventListener("click", closeDeleteModal); document.getElementById("btn-confirm-delete").addEventListener("click", confirmDeleteShortcut); // Configurações document.getElementById("btn-open-settings").addEventListener("click", openSettingsModal); document.getElementById("btn-close-settings").addEventListener("click", closeSettingsModal); document.getElementById("btn-save-settings").addEventListener("click", () => { const nameVal = document.getElementById("setting-user-name").value.trim(); if (nameVal) state.userName = nameVal; const cityVal = document.getElementById("setting-city").value.trim(); const cityChanged = (state.city || "") !== cityVal; state.city = cityVal; saveState(); applyAllSettings(); closeSettingsModal(); showToast("Configurações salvas!", "check"); if (cityChanged) refreshWeather(); scheduleSync(); scheduleSync(); }); // Plano de fundo e temas document.getElementById("btn-apply-bg").addEventListener("click", () => { const url = document.getElementById("setting-bg-url").value.trim(); if (url) { state.bgUrl = url; saveState(); applyTheme(); showToast("Papel de parede atualizado!", "image"); scheduleSync(); scheduleSync(); } }); document.querySelectorAll(".preset-bg").forEach(b => { b.addEventListener("click", () => { state.bgUrl = b.dataset.url; document.getElementById("setting-bg-url").value = state.bgUrl; saveState(); applyTheme(); showToast("Papel de parede atualizado!", "image"); scheduleSync(); }); }); document.getElementById("setting-bg-dim").addEventListener("input", (e) => { state.bgDim = e.target.value; document.getElementById("val-bg-dim").innerText = `${state.bgDim}%`; applyTheme(); }); document.getElementById("setting-bg-dim").addEventListener("change", () => { saveState(); scheduleSync(); }); document.querySelectorAll(".theme-color").forEach(c => { c.addEventListener("click", () => { state.accentColor = c.dataset.accent; state.accentHover = c.dataset.hover; saveState(); applyTheme(); showToast("Cor aplicada!", "palette"); scheduleSync(); }); }); document.getElementById("setting-custom-color").addEventListener("change", (e) => { state.accentColor = e.target.value; state.accentHover = e.target.value; saveState(); applyTheme(); }); document.querySelectorAll(".layout-btn").forEach(btn => { btn.addEventListener("click", () => { state.layout = btn.dataset.layout; saveState(); applyTheme(); renderShortcuts(); }); }); document.querySelectorAll(".size-btn").forEach(btn => { btn.addEventListener("click", () => { state.cardSize = btn.dataset.size; saveState(); applyTheme(); renderShortcuts(); }); }); // Export / Import document.getElementById("btn-export-json").addEventListener("click", exportConfiguration); document.getElementById("btn-copy-json").addEventListener("click", copyConfigurationToClipboard); document.getElementById("btn-paste-json").addEventListener("click", pasteJsonFromClipboard); document.getElementById("btn-close-paste").addEventListener("click", closePasteJsonModal); document.getElementById("btn-cancel-paste").addEventListener("click", closePasteJsonModal); document.getElementById("btn-confirm-paste").addEventListener("click", confirmPasteJson); document.getElementById("file-import-json").addEventListener("change", importConfiguration); attachTasksNotesListeners(); updateTasksBadge(); lucide.createIcons(); }); // ------------------------------------------------------------- // HELPERS DE DATA // ------------------------------------------------------------- function formatDatePtBR(dateStr) { if (!dateStr) return ''; try { const d = new Date(dateStr + 'T00:00:00'); return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch (e) { return dateStr; } } function isSameDateStr(a, b) { return a === b; } // ------------------------------------------------------------- // TAREFAS // ------------------------------------------------------------- function getTasksForDate(dateStr) { return (state.tasks || []).filter(t => t.date === dateStr).sort((a,b) => { const pa = { low:0, medium:1, high:2 }[a.priority||'medium']; const pb = { low:0, medium:1, high:2 }[b.priority||'medium']; if (pb !== pa) return pb - pa; return (b.createdAt||0) - (a.createdAt||0); }); } function getAllTasksSorted() { return (state.tasks || []).slice().sort((a,b) => { if (a.date !== b.date) return (a.date||'').localeCompare(b.date||''); const pa = { low:0, medium:1, high:2 }[a.priority||'medium']; const pb = { low:0, medium:1, high:2 }[b.priority||'medium']; if (pb !== pa) return pb - pa; return (b.createdAt||0) - (a.createdAt||0); }); } function taskHasOpenItems(t) { return Array.isArray(t.checklist) && t.checklist.some(it => !it.done); } function renderTasksList() { const listEl = document.getElementById('tasks-list'); if (!listEl) return; const tasks = getAllTasksSorted(); if (tasks.length === 0) { listEl.innerHTML = '

Nenhuma tarefa cadastrada.

'; return; } listEl.innerHTML = tasks.map(t => { const priority = t.priority || 'medium'; const pColors = { low:'bg-emerald-400/20 text-emerald-300', medium:'bg-amber-400/20 text-amber-300', high:'bg-rose-500/20 text-rose-300' }; const pLabel = { low:'Baixa', medium:'Média', high:'Alta' }[priority]; const open = taskHasOpenItems(t); const doneCount = (t.checklist||[]).filter(it=>it.done).length; const total = (t.checklist||[]).length; const checkedDot = open ? '' : 'bg-emerald-400'; return `

${escapeHtml(t.name || 'Sem nome')}

${pLabel} ${t.date ? `${formatDatePtBR(t.date)}` : ''} ${total > 0 ? `${doneCount}/${total}` : ''}
${t.desc ? `

${escapeHtml(t.desc)}

` : ''}
`; }).join(''); lucide.createIcons(); listEl.querySelectorAll('[data-task-id]').forEach(el => { const id = el.dataset.taskId; el.addEventListener('click', (e) => { if (e.target.closest('.btn-task-edit') || e.target.closest('.btn-task-delete')) return; openTaskDetail(id); }); el.querySelector('.btn-task-edit')?.addEventListener('click', (e) => { e.stopPropagation(); openTaskForm(id); }); el.querySelector('.btn-task-delete')?.addEventListener('click', (e) => { e.stopPropagation(); askDeleteTask(id); }); }); } function openTaskPanel() { renderTasksList(); document.getElementById('panel-tasks').classList.remove('hidden'); } function closeTaskPanel() { document.getElementById('panel-tasks').classList.add('hidden'); } function openTaskForm(taskIdOrPrefill = null) { const form = document.getElementById('form-task'); const title = document.getElementById('task-form-title'); const inputId = document.getElementById('input-task-id'); const inputName = document.getElementById('input-task-name'); const inputDesc = document.getElementById('input-task-desc'); const inputDate = document.getElementById('input-task-date'); const inputPriority = document.getElementById('input-task-priority'); const checklistEditor = document.getElementById('checklist-editor'); checklistEditor.innerHTML = ''; // Se for string, trata como ID (modo antigo) if (typeof taskIdOrPrefill === 'string') { const taskId = taskIdOrPrefill; const t = (state.tasks || []).find(x => x.id === taskId); title.innerHTML = ' Editar Tarefa'; inputId.value = taskId; inputName.value = t ? t.name || '' : ''; inputDesc.value = t ? t.desc || '' : ''; inputDate.value = t ? t.date || '' : ''; inputPriority.value = t ? t.priority || 'medium' : 'medium'; if (t && Array.isArray(t.checklist)) { t.checklist.forEach(it => addCheckItemEditor(it.id, it.text, it.done)); } } else { const prefill = taskIdOrPrefill || {}; title.innerHTML = ' Nova Tarefa'; inputId.value = prefill.id || ''; inputName.value = prefill.name || ''; inputDesc.value = prefill.desc || ''; inputDate.value = prefill.date || new Date().toISOString().slice(0,10); inputPriority.value = prefill.priority || 'medium'; if (Array.isArray(prefill.checklist)) { prefill.checklist.forEach(it => addCheckItemEditor(it.id, it.text, it.done)); } } if ((document.getElementById('checklist-editor').children.length) === 0) { addCheckItemEditor(); } document.getElementById('modal-task-form').classList.remove('hidden'); lucide.createIcons(); setTimeout(() => inputName.focus(), 50); } function closeTaskForm() { document.getElementById('modal-task-form').classList.add('hidden'); } function addCheckItemEditor(id = null, text = '', done = false) { const editor = document.getElementById('checklist-editor'); const itemId = id || generateId(); const wrap = document.createElement('div'); wrap.className = 'check-item'; if (done) wrap.classList.add('completed'); wrap.dataset.itemId = itemId; wrap.innerHTML = ` `; editor.appendChild(wrap); lucide.createIcons(); wrap.querySelector('.check-done').addEventListener('change', (e) => { wrap.classList.toggle('completed', e.target.checked); }); wrap.querySelector('.check-remove').addEventListener('click', () => { wrap.remove(); if (editor.children.length === 0) addCheckItemEditor(); }); } function collectChecklistFromEditor() { const items = []; document.querySelectorAll('#checklist-editor .check-item').forEach(w => { const done = w.querySelector('.check-done').checked; const text = w.querySelector('.check-item-text-input').value.trim(); if (text) { items.push({ id: w.dataset.itemId || generateId(), text, done }); } }); return items; } function handleTaskSubmit(e) { e.preventDefault(); const id = document.getElementById('input-task-id').value; const name = document.getElementById('input-task-name').value.trim(); const desc = document.getElementById('input-task-desc').value.trim(); const date = document.getElementById('input-task-date').value; const priority = document.getElementById('input-task-priority').value; const checklist = collectChecklistFromEditor(); if (!name || !date) return; if (id) { state.tasks = (state.tasks || []).map(t => t.id === id ? { ...t, name, desc, date, priority, checklist } : t); showToast('Tarefa atualizada!', 'check'); } else { const nt = { id: generateId(), name, desc, date, priority, checklist, createdAt: Date.now() }; state.tasks = [...(state.tasks || []), nt]; showToast('Tarefa adicionada!', 'clipboard-list'); } saveState(); closeTaskForm(); renderTasksList(); renderCalendar(); renderCalendarDayTasks(); updateTasksBadge(); } function askDeleteTask(id) { window._taskDeleteId = id; const t = (state.tasks || []).find(x => x.id === id); document.getElementById('task-delete-text').innerText = `Deseja excluir "${t ? (t.name||'esta tarefa') : 'esta tarefa'}"?`; document.getElementById('modal-task-delete').classList.remove('hidden'); } function closeTaskDelete() { delete window._taskDeleteId; document.getElementById('modal-task-delete').classList.add('hidden'); } function confirmDeleteTask() { const id = window._taskDeleteId; if (!id) return; state.tasks = (state.tasks || []).filter(t => t.id !== id); saveState(); closeTaskDelete(); renderTasksList(); renderCalendar(); renderCalendarDayTasks(); updateTasksBadge(); closeTaskDetail(); showToast('Tarefa excluída!', 'trash-2'); } function openTaskDetail(taskId) { const t = (state.tasks || []).find(x => x.id === taskId); const wrap = document.getElementById('task-detail-content'); if (!t) { closeTaskDetail(); return; } const priority = t.priority || 'medium'; const pColors = { low:'bg-emerald-400/20 text-emerald-300', medium:'bg-amber-400/20 text-amber-300', high:'bg-rose-500/20 text-rose-300' }; const pLabel = { low:'Baixa', medium:'Média', high:'Alta' }[priority]; const doneCount = (t.checklist||[]).filter(it=>it.done).length; const total = (t.checklist||[]).length; wrap.innerHTML = `

${escapeHtml(t.name||'Sem nome')}

${pLabel} ${t.date ? `Data: ${formatDatePtBR(t.date)}` : ''} ${total > 0 ? `Checklist: ${doneCount}/${total}` : ''}
${t.desc ? `
${escapeHtml(t.desc)}
` : ''} ${Array.isArray(t.checklist) && t.checklist.length > 0 ? `
${t.checklist.map(it => `
${escapeHtml(it.text)}
`).join('')}
` : ''}
`; document.getElementById('modal-task-detail').classList.remove('hidden'); lucide.createIcons(); wrap.querySelectorAll('.check-done-detail').forEach(cb => { cb.addEventListener('change', (e) => { const itemWrap = cb.closest('.check-item'); const itemId = itemWrap.dataset.itemId; const idxTask = (state.tasks||[]).findIndex(x => x.id === taskId); if (idxTask >= 0) { const t2 = state.tasks[idxTask]; t2.checklist = (t2.checklist||[]).map(it => it.id === itemId ? { ...it, done: e.target.checked } : it); state.tasks[idxTask] = t2; saveState(); itemWrap.classList.toggle('completed', e.target.checked); renderTasksList(); renderCalendar(); renderCalendarDayTasks(); updateTasksBadge(); } }); }); document.getElementById('btn-task-detail-edit').addEventListener('click', () => { closeTaskDetail(); openTaskForm(taskId); }); document.getElementById('btn-task-detail-delete').addEventListener('click', () => { closeTaskDetail(); askDeleteTask(taskId); }); } function closeTaskDetail() { document.getElementById('modal-task-detail').classList.add('hidden'); } function updateTasksBadge() { const dot = document.getElementById('tasks-dot-badge'); if (!dot) return; const hasAny = (state.tasks || []).length > 0; dot.classList.toggle('hidden', !hasAny); } // ------------------------------------------------------------- // NOTAS // ------------------------------------------------------------- function getAllNotesSorted() { return (state.notes || []).slice().sort((a,b) => (b.createdAt||0) - (a.createdAt||0)); } function renderNotesList() { const listEl = document.getElementById('notes-list'); if (!listEl) return; const notes = getAllNotesSorted(); if (notes.length === 0) { listEl.innerHTML = '

Nenhuma nota criada.

'; return; } listEl.innerHTML = notes.map(n => { const dt = n.createdAt ? new Date(n.createdAt).toLocaleString('pt-BR') : ''; const desc = (n.desc||'').replace(/\r\n/g, '\n'); return `

${escapeHtml(n.name || 'Sem título')}

${dt ? `${dt}` : ''}
${desc ? `

${escapeHtml(desc)}

` : ''}
`; }).join(''); lucide.createIcons(); listEl.querySelectorAll('[data-note-id]').forEach(el => { const id = el.dataset.noteId; el.addEventListener('click', (e) => { if (e.target.closest('.btn-note-edit') || e.target.closest('.btn-note-delete')) return; openNoteForm(id); }); el.querySelector('.btn-note-edit')?.addEventListener('click', (e) => { e.stopPropagation(); openNoteForm(id); }); el.querySelector('.btn-note-delete')?.addEventListener('click', (e) => { e.stopPropagation(); askDeleteNote(id); }); }); } function openNotesPanel() { renderNotesList(); document.getElementById('panel-notes').classList.remove('hidden'); } function closeNotesPanel() { document.getElementById('panel-notes').classList.add('hidden'); } function openNoteForm(noteId = null) { const title = document.getElementById('note-form-title'); const inputId = document.getElementById('input-note-id'); const inputName = document.getElementById('input-note-name'); const inputDesc = document.getElementById('input-note-desc'); const info = document.getElementById('note-created-info'); if (noteId) { const n = (state.notes || []).find(x => x.id === noteId); title.innerHTML = ' Editar Nota'; inputId.value = noteId; inputName.value = n ? n.name || '' : ''; inputDesc.value = n ? n.desc || '' : ''; if (n && n.createdAt) { info.innerText = 'Criada em ' + new Date(n.createdAt).toLocaleString('pt-BR'); info.classList.remove('hidden'); } else { info.classList.add('hidden'); } } else { title.innerHTML = ' Nova Nota'; inputId.value = ''; inputName.value = ''; inputDesc.value = ''; info.classList.add('hidden'); } document.getElementById('modal-note-form').classList.remove('hidden'); lucide.createIcons(); setTimeout(() => inputName.focus(), 50); } function closeNoteForm() { document.getElementById('modal-note-form').classList.add('hidden'); } function handleNoteSubmit(e) { e.preventDefault(); const id = document.getElementById('input-note-id').value; const name = document.getElementById('input-note-name').value.trim(); const desc = document.getElementById('input-note-desc').value.trim(); if (!name || !desc) return; if (id) { state.notes = (state.notes || []).map(n => n.id === id ? { ...n, name, desc } : n); showToast('Nota atualizada!', 'check'); } else { const nn = { id: generateId(), name, desc, createdAt: Date.now() }; state.notes = [...(state.notes || []), nn]; showToast('Nota criada!', 'sticky-note'); } saveState(); closeNoteForm(); renderNotesList(); } function askDeleteNote(id) { window._noteDeleteId = id; const n = (state.notes || []).find(x => x.id === id); document.getElementById('note-delete-text').innerText = `Deseja excluir "${n ? (n.name||'esta nota') : 'esta nota'}"?`; document.getElementById('modal-note-delete').classList.remove('hidden'); } function closeNoteDelete() { delete window._noteDeleteId; document.getElementById('modal-note-delete').classList.add('hidden'); } function confirmDeleteNote() { const id = window._noteDeleteId; if (!id) return; state.notes = (state.notes || []).filter(n => n.id !== id); saveState(); closeNoteDelete(); renderNotesList(); showToast('Nota excluída!', 'trash-2'); } function renderCalendarDayTasks() { const wrap = document.getElementById('cal-day-tasks'); const list = document.getElementById('cal-day-tasks-list'); if (!wrap || !list) return; const sel = window._calendarSelectedDay || new Date().toISOString().slice(0,10); const tasks = getTasksForDate(sel); if (tasks.length === 0) { list.innerHTML = ''; wrap.classList.remove('hidden'); return; } list.innerHTML = tasks.map(t => { const priority = t.priority || 'medium'; const pColors = { low:'bg-emerald-400/20 text-emerald-300', medium:'bg-amber-400/20 text-amber-300', high:'bg-rose-500/20 text-rose-300' }; const pLabel = { low:'Baixa', medium:'Média', high:'Alta' }[priority]; const open = taskHasOpenItems(t); const dot = open ? 'bg-amber-400' : 'bg-emerald-400'; return ``; }).join(''); wrap.classList.remove('hidden'); list.querySelectorAll('[data-task-id]').forEach(btn => { btn.addEventListener('click', () => openTaskDetail(btn.dataset.taskId)); }); } // ------------------------------------------------------------- // EVENT LISTENERS - TAREFAS E NOTAS // ------------------------------------------------------------- function attachTasksNotesListeners() { // Sidebar document.getElementById('btn-open-tasks')?.addEventListener('click', openTaskPanel); document.getElementById('btn-open-notes')?.addEventListener('click', openNotesPanel); // Tasks panel close document.getElementById('btn-close-tasks')?.addEventListener('click', closeTaskPanel); document.querySelector('[data-close-panel="panel-tasks"]')?.addEventListener('click', closeTaskPanel); // Task form document.getElementById('btn-new-task')?.addEventListener('click', () => openTaskForm()); document.getElementById('btn-close-task-form')?.addEventListener('click', closeTaskForm); document.getElementById('btn-cancel-task')?.addEventListener('click', closeTaskForm); document.getElementById('form-task')?.addEventListener('submit', handleTaskSubmit); document.getElementById('btn-add-check-item')?.addEventListener('click', () => addCheckItemEditor()); // Task detail document.getElementById('btn-close-task-detail')?.addEventListener('click', closeTaskDetail); // Task delete document.getElementById('btn-cancel-task-delete')?.addEventListener('click', closeTaskDelete); document.getElementById('btn-confirm-task-delete')?.addEventListener('click', confirmDeleteTask); // Add task from calendar selected day document.getElementById('btn-add-task-from-day')?.addEventListener('click', () => { const sel = window._calendarSelectedDay || new Date().toISOString().slice(0,10); openTaskForm({ date: sel }); }); // Notes panel close document.getElementById('btn-close-notes')?.addEventListener('click', closeNotesPanel); document.querySelector('[data-close-panel="panel-notes"]')?.addEventListener('click', closeNotesPanel); // Notes form document.getElementById('btn-new-note')?.addEventListener('click', () => openNoteForm()); document.getElementById('btn-close-note-form')?.addEventListener('click', closeNoteForm); document.getElementById('btn-cancel-note')?.addEventListener('click', closeNoteForm); document.getElementById('form-note')?.addEventListener('submit', handleNoteSubmit); // Note delete document.getElementById('btn-cancel-note-delete')?.addEventListener('click', closeNoteDelete); document.getElementById('btn-confirm-note-delete')?.addEventListener('click', confirmDeleteNote); } // ------------------------------------------------------------- // SINCRONIZAÇÃO NA NUVEM // ------------------------------------------------------------- function scheduleSync() { if (!authUser) return; if (syncTimer) clearTimeout(syncTimer); syncTimer = setTimeout(() => { pushSync(); }, 1500); } async function checkAuth() { try { const res = await fetch(API_BASE + "/auth/me", { credentials: "include" }); const data = await res.json(); if (data.logged) { authUser = data; updateSyncUI(); pullSync(); } else { authUser = null; updateSyncUI(); } } catch (e) { authUser = null; updateSyncUI(); } } function updateSyncUI() { const title = document.getElementById("sync-status-title"); const sub = document.getElementById("sync-status-sub"); const badge = document.getElementById("sync-badge"); const btnLogin = document.getElementById("btn-sync-login"); const btnSync = document.getElementById("btn-sync-now"); const btnLogout = document.getElementById("btn-sync-logout"); if (authUser) { if (title) title.innerText = "Conectado"; if (sub) sub.innerText = authUser.email || "Sincronização ativa"; if (badge) { badge.classList.remove("hidden"); badge.innerText = "Sincronizado"; } if (btnLogin) btnLogin.classList.add("hidden"); if (btnSync) { btnSync.classList.remove("disabled:opacity-40", "disabled:pointer-events-none"); btnSync.disabled = false; } if (btnLogout) btnLogout.classList.remove("hidden"); } else { if (title) title.innerText = "Não conectado"; if (sub) sub.innerText = "Faça login para sincronizar"; if (badge) badge.classList.add("hidden"); if (btnLogin) btnLogin.classList.remove("hidden"); if (btnSync) { btnSync.classList.add("disabled:opacity-40", "disabled:pointer-events-none"); btnSync.disabled = true; } if (btnLogout) btnLogout.classList.add("hidden"); } if (window.lucide) lucide.createIcons(); } async function pushSync() { if (!authUser) return; try { await fetch(API_BASE + "/sync", { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: state, version: Date.now() }) }); const badge = document.getElementById("sync-badge"); if (badge) { badge.innerText = "Sincronizado"; badge.classList.remove("hidden"); } } catch (e) {} } async function pullSync() { if (!authUser) return; try { const res = await fetch(API_BASE + "/sync", { credentials: "include" }); const json = await res.json(); if (json.data) { pendingCloudData = json.data; const m = document.getElementById("modal-sync-merge"); if (m) m.classList.remove("hidden"); } } catch (e) {} } function applyCloudData(data) { try { state = { ...DEFAULT_CONFIG, ...sanitizeState(data) }; if (!Array.isArray(state.categories) || state.categories.length === 0) { state.categories = [...DEFAULT_CONFIG.categories]; } saveState(); applyAllSettings(); renderTabs(); renderShortcuts(); populateCategorySelect(); if (document.getElementById("modal-sync-merge")) document.getElementById("modal-sync-merge").classList.add("hidden"); showToast("Dados da nuvem aplicados!", "cloud"); } catch (e) { showToast("Erro ao aplicar dados da nuvem", "alert-circle"); } } function openAuthModal() { const m = document.getElementById("modal-auth"); if (m) m.classList.remove("hidden"); if (window.lucide) lucide.createIcons(); } function closeAuthModal() { const m = document.getElementById("modal-auth"); if (m) m.classList.add("hidden"); } // Sincronização na Nuvem - eventos document.getElementById("btn-sync-login")?.addEventListener("click", openAuthModal); document.getElementById("btn-close-auth-modal")?.addEventListener("click", closeAuthModal); document.getElementById("modal-auth")?.addEventListener("click", (e) => { if (e.target.id === "modal-auth") closeAuthModal(); }); document.querySelectorAll(".auth-tab-btn").forEach(btn => { btn.addEventListener("click", () => { document.querySelectorAll(".auth-tab-btn").forEach(b => { b.classList.remove("bg-white/15", "text-white"); b.classList.add("text-slate-300"); }); btn.classList.add("bg-white/15", "text-white"); btn.classList.remove("text-slate-300"); const tab = btn.dataset.authTab; document.getElementById("form-login").classList.toggle("hidden", tab !== "login"); document.getElementById("form-register").classList.toggle("hidden", tab !== "register"); document.getElementById("login-error").classList.add("hidden"); document.getElementById("reg-error").classList.add("hidden"); if (window.lucide) lucide.createIcons(); }); }); document.getElementById("form-login")?.addEventListener("submit", async (e) => { e.preventDefault(); const email = document.getElementById("login-email").value; const password = document.getElementById("login-password").value; const errEl = document.getElementById("login-error"); try { const res = await fetch(API_BASE + "/auth/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); const data = await res.json(); if (!res.ok) { errEl.innerText = data.error || "Erro ao entrar"; errEl.classList.remove("hidden"); return; } errEl.classList.add("hidden"); closeAuthModal(); await checkAuth(); showToast("Logado com sucesso!", "log-in"); pullSync(); } catch (ex) { errEl.innerText = "Erro de conexão"; errEl.classList.remove("hidden"); } }); document.getElementById("form-register")?.addEventListener("submit", async (e) => { e.preventDefault(); const email = document.getElementById("reg-email").value; const password = document.getElementById("reg-password").value; const honeypot = document.getElementById("reg-honeypot").value; const errEl = document.getElementById("reg-error"); try { const res = await fetch(API_BASE + "/auth/register", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password, website: honeypot }) }); const data = await res.json(); if (!res.ok) { errEl.innerText = data.error || "Erro ao criar conta"; errEl.classList.remove("hidden"); return; } errEl.classList.add("hidden"); closeAuthModal(); await checkAuth(); showToast("Conta criada e logada!", "user-plus"); scheduleSync(); } catch (ex) { errEl.innerText = "Erro de conexão"; errEl.classList.remove("hidden"); } }); document.getElementById("btn-sync-logout")?.addEventListener("click", async () => { try { await fetch(API_BASE + "/auth/logout", { method: "POST", credentials: "include" }); } catch (e) {} authUser = null; updateSyncUI(); showToast("Desconectado", "log-out"); }); document.getElementById("btn-sync-now")?.addEventListener("click", async () => { if (!authUser) return; showToast("Sincronizando...", "refresh-cw"); await pushSync(); showToast("Sincronização enviada!", "cloud"); }); document.getElementById("modal-sync-merge")?.addEventListener("click", (e) => { if (e.target.id === "modal-sync-merge") { document.getElementById("modal-sync-merge").classList.add("hidden"); } }); document.getElementById("btn-sync-cancel")?.addEventListener("click", () => { document.getElementById("modal-sync-merge").classList.add("hidden"); }); document.getElementById("btn-sync-download")?.addEventListener("click", () => { if (pendingCloudData) applyCloudData(pendingCloudData); }); document.getElementById("btn-sync-upload")?.addEventListener("click", async () => { document.getElementById("modal-sync-merge").classList.add("hidden"); showToast("Enviando dados locais...", "upload-cloud"); await pushSync(); showToast("Dados enviados para a nuvem!", "cloud"); }); checkAuth();