Files
initpage/web/app.js

2393 lines
88 KiB
JavaScript

"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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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}&current=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 = `<span>${day}</span>${hasTasks ? '<span class="cal-task-dot"></span>' : ''}`;
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 = '<i data-lucide="plus" class="w-4 h-4 text-accent"></i>';
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 = `<span>${escapeHtml(label)}</span>`;
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 = `
<div class="w-full ${bannerHeight} flex flex-col items-center justify-center">
<div class="w-10 h-10 rounded-full glass-card flex items-center justify-center group-hover:scale-110 group-hover:bg-accent group-hover:text-white transition-all duration-300">
<i data-lucide="plus" class="w-5 h-5 text-accent group-hover:text-white transition-colors"></i>
</div>
</div>
<div class="p-3 pt-2 text-center w-full">
<h4 class="text-xs sm:text-sm font-semibold text-slate-300 group-hover:text-white transition-colors">Adicionar Atalho</h4>
</div>
`;
} else if (state.layout === "dock") {
card.classList.add("p-2.5", "flex", "flex-col", "items-center", "w-20");
card.innerHTML = `
<div class="w-11 h-11 rounded-2xl glass-card flex items-center justify-center group-hover:scale-110 group-hover:bg-accent transition-all duration-300">
<i data-lucide="plus" class="w-5 h-5 text-accent group-hover:text-white transition-colors"></i>
</div>
<span class="text-[10px] font-semibold text-slate-300 mt-1.5 truncate w-full text-center">Adicionar</span>
`;
} else if (state.layout === "list") {
card.classList.add("flex", "items-center", "p-4", "gap-4", "justify-center");
card.innerHTML = `
<div class="w-8 h-8 rounded-xl glass-card flex items-center justify-center group-hover:scale-110 group-hover:bg-accent transition-all duration-300">
<i data-lucide="plus" class="w-4 h-4 text-accent group-hover:text-white transition-colors"></i>
</div>
<span class="text-xs sm:text-sm font-semibold text-slate-300 group-hover:text-white transition-colors">Adicionar novo atalho nesta categoria</span>
`;
}
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
? `<img src="${escapeHtml(safeCover)}" alt="${safeName}" loading="lazy" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('bg-gradient-to-tr','from-slate-800','via-indigo-950','to-slate-800');">`
: `<div class="w-full h-full bg-gradient-to-tr from-slate-800 via-indigo-950 to-slate-800 flex items-center justify-center">
<i data-lucide="globe" class="w-8 h-8 text-white/20"></i>
</div>`;
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 = `
<div class="w-full ${bannerHeight} relative overflow-hidden bg-slate-900">
${coverHtml}
<div class="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-black/20"></div>
<div class="shortcut-icon-box absolute bottom-2 left-3 w-8 h-8 rounded-xl glass-panel p-1.5 flex items-center justify-center shadow-lg">
<img src="${escapeHtml(safeIcon)}" alt="" class="w-full h-full object-contain" onerror="this.onerror=null; this.src='https://www.google.com/s2/favicons?domain=example.com';">
</div>
<div class="absolute top-2 right-2 px-2 py-0.5 rounded-md glass-panel text-[10px] text-slate-300 opacity-0 group-hover:opacity-100 transition-opacity">
${categoryName}
</div>
</div>
<div class="p-3 pt-2">
<h4 class="text-xs sm:text-sm font-bold text-white truncate group-hover:text-accent transition-colors">${safeName}</h4>
<p class="text-[10px] sm:text-xs text-slate-400 line-clamp-3 mt-0.5">${safeDesc}</p>
</div>
`;
} else if (state.layout === "dock") {
card.classList.add("p-2.5", "flex", "flex-col", "items-center", "w-20");
card.innerHTML = `
<div class="shortcut-icon-box w-11 h-11 rounded-2xl glass-panel p-2 flex items-center justify-center shadow-md relative">
<img src="${escapeHtml(safeIcon)}" alt="" class="w-full h-full object-contain" onerror="this.onerror=null; this.src='https://www.google.com/s2/favicons?domain=example.com';">
</div>
<span class="text-[10px] font-semibold text-slate-200 mt-1.5 truncate w-full text-center">${safeName}</span>
`;
} else if (state.layout === "list") {
card.classList.add("flex", "items-center", "p-3", "gap-4");
card.innerHTML = `
<div class="w-14 h-14 rounded-xl overflow-hidden relative flex-shrink-0 bg-slate-900 border border-white/10">
${coverHtml}
<div class="absolute inset-0 bg-black/20"></div>
<img src="${escapeHtml(safeIcon)}" class="w-5 h-5 absolute bottom-1.5 right-1.5 glass-panel p-0.5 rounded shadow" onerror="this.onerror=null; this.src='https://www.google.com/s2/favicons?domain=example.com';">
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<h4 class="text-sm font-bold text-white truncate">${safeName}</h4>
<span class="px-2 py-0.5 rounded-full bg-white/10 text-[10px] text-slate-300">${categoryName}</span>
</div>
<p class="text-xs text-slate-400 line-clamp-3">${safeDesc}</p>
</div>
`;
}
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 = '<i data-lucide="edit-2" class="w-3 h-3"></i>';
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 = '<i data-lucide="trash-2" class="w-3 h-3"></i>';
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 = `
<div class="flex items-center gap-2">
<i data-lucide="tag" class="w-3.5 h-3.5 text-accent"></i>
<span class="font-semibold text-white">${escapeHtml(cat.name)}</span>
<span class="text-[10px] text-slate-400">(${count} atalhos)</span>
</div>
<div class="flex items-center gap-1">
<button type="button" class="btn-edit-cat p-1 text-slate-300 hover:text-white rounded hover:bg-white/20 transition" title="Editar" aria-label="Editar categoria ${escapeHtml(cat.name)}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
<button type="button" class="btn-del-cat p-1 text-slate-400 hover:text-rose-400 rounded hover:bg-rose-500/20 transition" title="Excluir" aria-label="Excluir categoria ${escapeHtml(cat.name)}">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
`;
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 = '<i data-lucide="edit" class="w-5 h-5 text-accent"></i> 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 = '<i data-lucide="bookmark-plus" class="w-5 h-5 text-accent"></i> 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 = '<p class="text-xs text-slate-400 text-center mt-6">Nenhuma tarefa cadastrada.</p>';
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 `
<div class="group flex flex-col gap-1.5 p-3 rounded-2xl glass-card cursor-pointer border border-white/10 hover:border-accent transition" data-task-id="${escapeHtml(t.id)}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<h4 class="text-xs font-bold text-white truncate flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full ${checkedDot || 'bg-amber-400'}"></span>
${escapeHtml(t.name || 'Sem nome')}
</h4>
<div class="flex items-center gap-2 mt-1 flex-wrap">
<span class="px-2 py-0.5 rounded-full text-[10px] ${pColors[priority]}">${pLabel}</span>
${t.date ? `<span class="px-2 py-0.5 rounded-full bg-white/10 text-[10px] text-slate-300">${formatDatePtBR(t.date)}</span>` : ''}
${total > 0 ? `<span class="px-2 py-0.5 rounded-full bg-white/10 text-[10px] text-slate-300">${doneCount}/${total}</span>` : ''}
</div>
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<button class="btn-task-edit p-1 rounded hover:bg-white/20 text-slate-300 hover:text-white" aria-label="Editar tarefa">
<i data-lucide="edit-2" class="w-3 h-3"></i>
</button>
<button class="btn-task-delete p-1 rounded hover:bg-rose-500/20 text-slate-400 hover:text-rose-400" aria-label="Excluir tarefa">
<i data-lucide="trash-2" class="w-3 h-3"></i>
</button>
</div>
</div>
${t.desc ? `<p class="text-[10px] text-slate-400 line-clamp-2 whitespace-pre-wrap">${escapeHtml(t.desc)}</p>` : ''}
</div>
`;
}).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 = '<i data-lucide="edit" class="w-5 h-5 text-accent"></i> 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 = '<i data-lucide="clipboard-plus" class="w-5 h-5 text-accent"></i> 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 = `
<input type="checkbox" class="check-done" ${done ? 'checked' : ''}>
<input type="text" class="check-item-text-input" placeholder="Item do checklist..." value="${escapeHtml(text)}">
<button type="button" class="check-remove p-1 rounded hover:bg-rose-500/20 text-slate-500 hover:text-rose-400" aria-label="Remover item">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
`;
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 = `
<h3 class="text-base font-bold text-white pr-8 mb-2">${escapeHtml(t.name||'Sem nome')}</h3>
<div class="flex items-center gap-2 flex-wrap mb-3">
<span class="px-2 py-0.5 rounded-full text-[10px] ${pColors[priority]}">${pLabel}</span>
${t.date ? `<span class="px-2 py-0.5 rounded-full bg-white/10 text-[10px] text-slate-300">Data: ${formatDatePtBR(t.date)}</span>` : ''}
${total > 0 ? `<span class="px-2 py-0.5 rounded-full bg-white/10 text-[10px] text-slate-300">Checklist: ${doneCount}/${total}</span>` : ''}
</div>
${t.desc ? `<div class="text-xs text-slate-300 whitespace-pre-wrap mb-3 p-3 rounded-xl bg-white/5 border border-white/10 max-h-40 overflow-y-auto">${escapeHtml(t.desc)}</div>` : ''}
${Array.isArray(t.checklist) && t.checklist.length > 0 ? `
<div class="space-y-1.5 mb-3">
${t.checklist.map(it => `
<div class="check-item ${it.done ? 'completed' : ''}" data-item-id="${escapeHtml(it.id)}">
<input type="checkbox" class="check-done-detail" ${it.done ? 'checked' : ''}>
<span class="check-item-text text-xs text-slate-200 flex-1">${escapeHtml(it.text)}</span>
</div>
`).join('')}
</div>
` : ''}
<div class="flex items-center justify-end gap-2 pt-3 border-t border-white/10">
<button id="btn-task-detail-edit" class="px-3 py-1.5 rounded-xl bg-white/10 hover:bg-white/20 text-xs font-semibold text-white flex items-center gap-1.5">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i> Editar
</button>
<button id="btn-task-detail-delete" class="px-3 py-1.5 rounded-xl bg-rose-600 hover:bg-rose-500 text-xs font-semibold text-white flex items-center gap-1.5">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Excluir
</button>
</div>
`;
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 = '<p class="text-xs text-slate-400 text-center mt-6">Nenhuma nota criada.</p>';
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 `
<div class="group flex flex-col gap-1.5 p-3 rounded-2xl glass-card cursor-pointer border border-white/10 hover:border-accent/40 transition" data-note-id="${escapeHtml(n.id)}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<h4 class="text-xs font-bold text-white truncate">${escapeHtml(n.name || 'Sem título')}</h4>
${dt ? `<span class="text-[10px] text-slate-500">${dt}</span>` : ''}
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<button class="btn-note-edit p-1 rounded hover:bg-white/20 text-slate-300 hover:text-white" aria-label="Editar nota">
<i data-lucide="edit-2" class="w-3 h-3"></i>
</button>
<button class="btn-note-delete p-1 rounded hover:bg-rose-500/20 text-slate-400 hover:text-rose-400" aria-label="Excluir nota">
<i data-lucide="trash-2" class="w-3 h-3"></i>
</button>
</div>
</div>
${desc ? `<p class="text-[10px] text-slate-400 whitespace-pre-wrap line-clamp-4">${escapeHtml(desc)}</p>` : ''}
</div>
`;
}).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 = '<i data-lucide="sticky-note" class="w-5 h-5 text-amber-400"></i> 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 = '<i data-lucide="sticky-note" class="w-5 h-5 text-amber-400"></i> 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 `<button type="button" data-task-id="${escapeHtml(t.id)}" class="w-full text-left flex flex-col gap-0.5 p-2 rounded-xl glass-card border border-white/10 hover:border-accent transition">
<span class="text-[11px] font-semibold text-white truncate flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full ${dot}"></span>
${escapeHtml(t.name||'Sem nome')}
</span>
<div class="flex items-center gap-1.5 flex-wrap pl-2">
<span class="px-1.5 py-0.5 rounded-md ${pColors[priority]} text-[9px]">${pLabel}</span>
${(t.checklist||[]).length ? `<span class="px-1.5 py-0.5 rounded-md bg-white/10 text-[9px] text-slate-300">${(t.checklist||[]).filter(it=>it.done).length}/${(t.checklist||[]).length}</span>` : ''}
</div>
</button>`;
}).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();