2393 lines
88 KiB
JavaScript
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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
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 = `<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();
|