Rio de Janeiro
TOP 3 DO DIA
EM SEGUIDA
Ver tudo →
TODAS ABERTAS Ver tudo →
ESCORREGANDO ver →
ROTINAS ver →
RELEMBRAR
REVISAR
ESCORREGANDO ver →
ROTINAS ver →
RELEMBRAR
REVISAR
Ordenar:
Ordenar:
PERFIL
Nome
Cargo
Conta Google
🎤 VOZ E IA
Groq API Key
Whisper (voz) + Llama 3.3 (chat) · console.groq.com
Status
GOOGLE CALENDAR
Sincronizar agenda
Eventos aparecem em "Em Seguida"
Não conectado
VERSÃO
Fast Fluent OS
APARÊNCIA
Tema
Cores de categoria
Aulas
Admin
Cobranças
Pessoal
IDIOMA
Idioma do app
🔔 NOTIFICAÇÕES
Rotinas — Manhã
Lembrete de rotinas matinais
Rotinas — Tarde
Rotinas — Noite
Lembrete de compromissos
15 min antes de cada evento do Calendar
LAYOUT E TEMA
Modo compacto
Menos espaço, mais conteúdo
Cor de destaque
Cor dos botões e destaques
DADOS
Exportar
Backup JSON de todos os dados
Limpar tudo
Remove todos os dados permanentemente
O que deseja criar?
Tarefa
Adicionar uma nova tarefa
Projeto
Criar um novo projeto
Nota
Salvar uma nota ou citação
Rotina
Adicionar uma nova rotina
🔔
Sincronizado
Soltar para atualizar
Fast Fluent OS
T
Grafo de notas
Atividade recente
Nenhuma atividade ainda
📵 Sem conexão — trabalhando offline
Nova tarefa
Nova nota
Captura por voz
Assistente de voz
Toque no microfone para falar
VOCÊ DISSE
RESPOSTA
' + '
'; }).join('') + '
'; if (!allStarred.length) { el.innerHTML += '
'+t('today_top3_empty')+'
'; } } // UP NEXT — tasks with time today function renderUpNext() { const todayStr = dk(curDate); const tasks = getTasks().filter(t => !t.done && t.date === todayStr && t.time).sort((a,b) => a.time > b.time ? 1 : -1); const el = document.getElementById('upnext-list'); if (!el) return; if (!tasks.length) { el.innerHTML = ''; return; } el.innerHTML = tasks.map(t => `
${t.time}
${t.name}
${t.notes ? `
${t.notes.substring(0,60)}
` : ''}
`).join(''); } // ALL TASKS today function renderAllTasks() { const todayStr = dk(curDate); const tasks = getTasks().filter(t => !t.done && (t.date === todayStr || !t.date)); const el = document.getElementById('all-tasks-list'); const cnt = document.getElementById('tasks-count'); if (cnt) cnt.textContent = '· '+tasks.length; if (!el) return; if (!tasks.length) { el.innerHTML = '
'+t('today_no_tasks')+'
'; return; } el.innerHTML = tasks.sort((a,b) => { if(a.time && b.time) return a.time > b.time ? 1 : -1; if(a.time) return -1; if(b.time) return 1; return 0; }).map(t => taskRow(t)).join(''); } function taskRow(t) { const cat = CAT_LABELS[t.cat] || ''; const catColor = getCatColor(t.cat); const todayStr = dk(today0()); const overdue = t.date && t.date < todayStr && !t.done; return `
${t.name}
${t.date ? `${t.date}${t.time?' '+t.time:''}` : ''} ${t.cat ? `${cat}` : ''}
`; } const CAT_LABELS = { aula:'Aulas', admin:'Admin', cobranca:'Cobranças', pessoal:'Pessoal' }; function getCatColor(cat) { const colors = { aula:'var(--cat-aula)', admin:'var(--cat-admin)', cobranca:'var(--cat-cobranca)', pessoal:'var(--cat-pessoal)' }; return colors[cat] || 'var(--text3)'; } // SLIPPING — tasks not updated in 3+ days function renderSlipping() { const el = document.getElementById('slipping-list'); if (!el) return; const todayStr = dk(today0()); const slipping = getTasks().filter(t => { if (t.done) return false; if (!t.updated) return false; const days = Math.floor((new Date(todayStr) - new Date(t.updated)) / 86400000); return days >= 3; }).slice(0,5); if (!slipping.length) { el.innerHTML = '
'+t('today_no_slip')+'
'; return; } el.innerHTML = slipping.map(t => { const days = Math.floor((new Date(todayStr) - new Date(t.updated)) / 86400000); return `
${t.name.substring(0,35)}
${days}d
`; }).join(''); } // ROUTINES TODAY function renderRoutinesToday() { const el = document.getElementById('routines-today'); if (!el) return; const routines = getRoutines(); if (!routines.length) { el.innerHTML = '
'+t('today_no_routine')+'
'; return; } const todayStr = dk(curDate); const periods = [ {key:'manha', label:'☀️ MANHÃ'}, {key:'tarde', label:'🌤 TARDE'}, {key:'noite', label:'🌙 NOITE'} ]; let html = ''; periods.forEach(p => { const pr = routines.filter(r => r.period === p.key); if (!pr.length) return; html += `
${p.label}
`; pr.forEach(r => { const done = (r.completions||[]).includes(todayStr); const streak = getStreak(r); html += `
${r.name}
${streak7(r,todayStr)}
`; }); }); el.innerHTML = html || '
Sem rotinas.
'; } function streak7(r, todayStr) { let html = ''; for (let i = 6; i >= 0; i--) { const d = new Date(todayStr); d.setDate(d.getDate()-i); const ds = dk(d); const hit = (r.completions||[]).includes(ds); const past = ds < todayStr; html += `
`; } return html; } function getStreak(r) { const comps = (r.completions||[]).sort().reverse(); let streak = 0; let d = today0(); for (let i = 0; i < 365; i++) { if (comps.includes(dk(d))) { streak++; d.setDate(d.getDate()-1); } else break; } return streak; } // RESURFACING function renderResurfacing() { const items = getLib().filter(l => l.body && l.body.length > 20); const el = document.getElementById('resurf-content'); const sec = document.getElementById('resurf-sec'); if (!el || !items.length) { if(sec) sec.style.display='none'; return; } if(sec) sec.style.display='block'; const item = items[Math.floor(new Date().getDate() % items.length)]; el.innerHTML = `
"${item.body.substring(0,200)}"
— ${item.title||'Biblioteca'}
`; } // NEEDS REVIEW function renderReview() { const el = document.getElementById('review-list'); const cnt = document.getElementById('review-count'); if (!el) return; const items = getLib().filter(l => l.review); if (cnt) cnt.textContent = items.length ? '· '+items.length : ''; if (!items.length) { el.innerHTML = '
'+t('today_no_review')+'
'; return; } el.innerHTML = items.map(l => `
${(l.title||l.body||'').substring(0,80)}
Marcar como revisado ✓
`).join(''); } function clearReview(id) { const lib = getLib(); const item = lib.find(l => l.id === id); if (item) { item.review = false; setLib(lib); render(); toast(t('reviewed')); } } // ═══════════════════════════════════════════ // TASKS CRUD // ═══════════════════════════════════════════ function openAddTask() { editTaskId = null; _editSubtasks = []; _editComments = []; var fields = ['t-name','t-date','t-time','t-cat','t-notes','t-tags','t-comment-input']; fields.forEach(function(id) { var el=document.getElementById(id); if(el) el.value = id==='t-date' ? dk(today0()) : ''; }); var _recEl=document.getElementById('t-recur'); if(_recEl)_recEl.value=''; document.getElementById('t-delete-btn').style.display = 'none'; renderSubtasksModal(); renderCommentsModal(); openMo('mo-task'); setTimeout(function() { document.getElementById('t-name').focus(); }, 100); } function openEditTask(id) { var t = getTasks().find(function(x){return x.id===id;}); if (!t) return; editTaskId = id; _editSubtasks = (t.subtasks||[]).map(function(s){return Object.assign({},s);}); _editComments = (t.comments||[]).map(function(c){return Object.assign({},c);}); document.getElementById('mo-task-title').textContent = 'Editar Tarefa'; document.getElementById('t-name').value = t.name || ''; document.getElementById('t-date').value = t.date || ''; document.getElementById('t-time').value = t.time || ''; document.getElementById('t-cat').value = t.cat || ''; document.getElementById('t-notes').value = t.notes || ''; if(document.getElementById('t-recur')) document.getElementById('t-recur').value = t.recur||''; document.getElementById('t-tags').value = (t.tags||[]).join(', '); document.getElementById('t-delete-btn').style.display = 'inline-flex'; renderSubtasksModal(); renderCommentsModal(); openMo('mo-task'); } function saveTask() { const name = document.getElementById('t-name').value.trim(); if (!name) return; const tasks = getTasks(); var tagsRaw = document.getElementById('t-tags').value; var tagsArr = tagsRaw ? tagsRaw.split(',').map(function(t){return t.trim();}).filter(Boolean) : []; var data = { name: name, date: document.getElementById('t-date').value, time: document.getElementById('t-time').value, cat: document.getElementById('t-cat').value, notes: document.getElementById('t-notes').value, recur: (document.getElementById('t-recur')||{value:''}).value, subtasks: _editSubtasks.filter(function(s){return s.name.trim();}), tags: tagsArr, comments: _editComments, updated: dk(today0()) }; if (editTaskId) { const t = tasks.find(x => x.id === editTaskId); if (t) Object.assign(t, data); } else { tasks.push({ id: uid(), ...data, done: false, starred: false, created: Date.now() }); } setTasks(tasks); closeMo('mo-task'); render(); toast(t('saved')); } function deleteTask() { if (!editTaskId) return; if (!confirm(t('confirm_delete_task'))) return; setTasks(getTasks().filter(t => t.id !== editTaskId)); editTaskId = null; closeMo('mo-task'); render(); toast(t('deleted')); } function toggleTask(id) { const tasks = getTasks(); const t = tasks.find(x => x.id === id); if (t) { t.done = !t.done; t.updated = dk(today0()); } setTasks(tasks); render(); } function toggleStar(id) { const tasks = getTasks(); const t = tasks.find(x => x.id === id); if (t) t.starred = !t.starred; setTasks(tasks); render(); } function updateOverdueBadge() { const todayStr = dk(today0()); const n = getTasks().filter(t => !t.done && t.date && t.date < todayStr).length; const badge = document.getElementById('overdue-badge'); if (badge) { badge.textContent = n; badge.style.display = n ? 'inline' : 'none'; } } // TASKS PAGE function setTaskFilter(f, btn) { taskFilter = f; document.querySelectorAll('#page-tasks .fb').forEach(function(b){b.classList.remove('active');}); if (btn) btn.classList.add('active'); renderTasks(); } function renderTasks() { var tasks = getTasks(); var todayStr = dk(today0()); var week = getWeekRange(); var month = getMonthRange(); if (taskFilter === 'open') tasks = tasks.filter(function(t){return !t.done;}); else if (taskFilter === 'done') tasks = tasks.filter(function(t){return t.done;}); else if (taskFilter === 'overdue') tasks = tasks.filter(function(t){return !t.done&&t.date&&t.date=week.start&&t.date<=week.end;}); else if (taskFilter === 'month') tasks = tasks.filter(function(t){return !t.done&&t.date&&t.date>=month.start&&t.date<=month.end;}); else if (['aula','admin','cobranca','pessoal'].includes(taskFilter)) tasks = tasks.filter(function(t){return t.cat===taskFilter;}); // Search filter var q = getTaskSearchQuery(); if (q) { tasks = tasks.filter(function(task) { return (task.name||'').toLowerCase().includes(q) || (task.notes||'').toLowerCase().includes(q) || (task.tags||[]).some(function(tag){return tag.toLowerCase().includes(q);}) || (task.comments||[]).some(function(c){return (c.text||'').toLowerCase().includes(q);}); }); } var el = document.getElementById('tasks-list'); if (!el) return; if (!tasks.length) { el.innerHTML = '
'+t('tasks_none')+'
'; return; } tasks = applySort(tasks); el.innerHTML = tasks.map(function(task){return taskRowFull(task);}).join(''); setTimeout(initDragAndDrop, 50); } // ═══════════════════════════════════════════ // ROUTINES CRUD // ═══════════════════════════════════════════ function openAddRoutine() { editRoutineId = null; document.getElementById('mo-routine-title').textContent = 'Nova Rotina'; document.getElementById('r-name').value = ''; document.getElementById('r-period').value = 'manha'; document.getElementById('r-goal').value = '7'; document.getElementById('r-delete-btn').style.display = 'none'; openMo('mo-routine'); setTimeout(() => document.getElementById('r-name').focus(), 100); } function openEditRoutine(id) { const r = getRoutines().find(x => x.id === id); if (!r) return; editRoutineId = id; document.getElementById('mo-routine-title').textContent = 'Editar Rotina'; document.getElementById('r-name').value = r.name || ''; document.getElementById('r-period').value = r.period || 'manha'; document.getElementById('r-goal').value = r.goal || '7'; document.getElementById('r-delete-btn').style.display = 'inline-flex'; openMo('mo-routine'); } function saveRoutine() { const name = document.getElementById('r-name').value.trim(); if (!name) return; const routines = getRoutines(); const data = { name, period: document.getElementById('r-period').value, goal: parseInt(document.getElementById('r-goal').value)||7 }; if (editRoutineId) { const r = routines.find(x => x.id === editRoutineId); if (r) Object.assign(r, data); } else { routines.push({ id: uid(), ...data, completions: [] }); } setRoutines(routines); closeMo('mo-routine'); render(); toast(t('saved')); } function deleteRoutine() { if (!editRoutineId) return; if (!confirm(t('confirm_delete_routine'))) return; setRoutines(getRoutines().filter(r => r.id !== editRoutineId)); editRoutineId = null; closeMo('mo-routine'); render(); toast(t('deleted')); } function toggleRoutine(id) { const routines = getRoutines(); const r = routines.find(x => x.id === id); if (!r) return; const todayStr = dk(curDate); r.completions = r.completions || []; if (r.completions.includes(todayStr)) { r.completions = r.completions.filter(d => d !== todayStr); } else { r.completions.push(todayStr); } setRoutines(routines); render(); } function renderRoutines() { const routines = getRoutines(); const el = document.getElementById('routines-list'); if (!el) return; if (!routines.length) { el.innerHTML = '
Nenhuma rotina.
'; return; } const todayStr = dk(today0()); const periods = [{key:'manha',label:'☀️ Manhã'},{key:'tarde',label:'🌤 Tarde'},{key:'noite',label:'🌙 Noite'}]; let html = ''; periods.forEach(p => { const pr = routines.filter(r => r.period === p.key); if (!pr.length) return; html += `
${p.label}
`; pr.forEach(r => { const done = (r.completions||[]).includes(todayStr); const streak = getStreak(r); html += `
${r.name}
🔥 ${streak} dias · meta: ${r.goal||7} dias
${streak7(r,todayStr)}
`; }); }); el.innerHTML = html; } // ═══════════════════════════════════════════ // PROJECTS CRUD // ═══════════════════════════════════════════ function openAddProject() { editProjectId = null; document.getElementById('mo-project-title').textContent = 'Novo Projeto'; ['p-name','p-desc','p-deadline'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); document.getElementById('p-status').value = 'ativo'; document.getElementById('p-delete-btn').style.display = 'none'; openMo('mo-project'); setTimeout(() => document.getElementById('p-name').focus(), 100); } function openEditProject(id) { const p = getProjects().find(x => x.id === id); if (!p) return; editProjectId = id; document.getElementById('mo-project-title').textContent = 'Editar Projeto'; document.getElementById('p-name').value = p.name || ''; document.getElementById('p-desc').value = p.desc || ''; document.getElementById('p-deadline').value = p.deadline || ''; document.getElementById('p-status').value = p.status || 'ativo'; document.getElementById('p-delete-btn').style.display = 'inline-flex'; openMo('mo-project'); } function saveProject() { const name = document.getElementById('p-name').value.trim(); if (!name) return; const projects = getProjects(); const data = { name, desc: document.getElementById('p-desc').value, deadline: document.getElementById('p-deadline').value, status: document.getElementById('p-status').value, updated: dk(today0()) }; if (editProjectId) { const p = projects.find(x => x.id === editProjectId); if (p) Object.assign(p, data); } else { projects.push({ id: uid(), ...data, created: Date.now() }); } setProjects(projects); closeMo('mo-project'); render(); toast(t('saved')); } function deleteProject() { if (!editProjectId) return; if (!confirm(t('confirm_delete_project'))) return; setProjects(getProjects().filter(p => p.id !== editProjectId)); editProjectId = null; closeMo('mo-project'); render(); toast(t('deleted')); } function renderProjects() { const projects = getProjects(); const el = document.getElementById('projects-list'); if (!el) return; if (!projects.length) { el.innerHTML = '
'+t('projects_none')+'
'; return; } const statusColors = { ativo: 'var(--green)', pausado: 'var(--text3)', concluido: 'var(--blue)' }; el.innerHTML = projects.map(function(p) { var sc = statusColors[p.status]||'var(--text3)'; return '
' + '
' + '
' + '
'+escHtml(p.name)+'
' + (p.desc ? '
'+escHtml(p.desc.substring(0,80))+'
' : '') + (p.deadline ? '
📅 '+p.deadline+'
' : '') + '
' + ''+p.status+'' + '
'; }).join(''); } // ═══════════════════════════════════════════ // LIBRARY CRUD // ═══════════════════════════════════════════ function setLibFilter(f, btn) { libFilter = f; document.querySelectorAll('#page-library .filter-bar .fb').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); renderLibrary(); } function openAddLib(type){openNoteEditor(null,type||'nota');} function openEditLib(id){openNoteEditor(id);} function saveLib() { const title = document.getElementById('l-title').value.trim(); const body = document.getElementById('l-body').value.trim(); if (!title && !body) return; const lib = getLib(); const data = { title, body, tags: document.getElementById('l-tags').value, review: document.getElementById('l-review').value === '1', type: libFilter, date: dk(today0()) }; if (editLibId) { const l = lib.find(x => x.id === editLibId); if (l) Object.assign(l, data); } else { lib.push({ id: uid(), ...data }); } setLib(lib); closeMo('mo-lib'); render(); toast(t('saved')); } function deleteLib() { if (!editLibId) return; if (!confirm(t('confirm_delete_lib'))) return; setLib(getLib().filter(l => l.id !== editLibId)); editLibId = null; closeMo('mo-lib'); render(); toast(t('deleted')); } function renderLibrary() { const items = getLib().filter(l => l.type === libFilter); const el = document.getElementById('library-list'); if (!el) return; if (!items.length) { el.innerHTML = '
'+t('lib_none')+'
'; return; } el.innerHTML = items.map(function(l) { return '
' + '
' + (l.title ? '
'+escHtml(l.title)+'
' : '') + (l.review ? 'REVISAR' : '') + '
' + (l.body ? '
'+escHtml(l.body.substring(0,140))+(l.body.length>140?'...':'')+'
' : '') + (l.tags ? '
'+l.tags.split(',').filter(Boolean).map(function(tg){return ''+escHtml(tg.trim())+'';}).join('')+'
' : '') + '
'+(l.date||'')+'
' + '
'; }).join(''); } // ═══════════════════════════════════════════ // SETTINGS // ═══════════════════════════════════════════ function openSettings() { goTo('settings'); } function renderSettings() { var vl = document.getElementById('app-version-label'); if (vl) vl.textContent = 'v' + APP_VERSION + ' · ' + APP_BUILD; updateNotifToggles(); initCompactMode(); initAccentColor(); const profile = DB.get('profile') || {}; updateGroqStatus(); const nameEl = document.getElementById('cfg-name'); const roleEl = document.getElementById('cfg-role'); if (nameEl) nameEl.value = profile.name || ''; if (roleEl) roleEl.value = profile.role || ''; const emailEl = document.getElementById('cfg-email-lbl'); if (emailEl && window._fbUser) emailEl.textContent = window._fbUser.email || '—'; updateThemeBtns(); updateLangBtns(); updateCatColorInputs(); gcalUpdateUI(); } function saveProfile() { const name = document.getElementById('cfg-name').value.trim(); const role = document.getElementById('cfg-role').value.trim(); DB.set('profile', { name, role }); const sb = document.getElementById('sb-user'); if (sb) sb.textContent = name || (window._fbUser ? window._fbUser.displayName : '—'); toast(t('profile_saved')); } function exportData() { const data = { tasks: getTasks(), routines: getRoutines(), projects: getProjects(), library: getLib(), exported: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'fastfluent-backup-'+dk(today0())+'.json'; a.click(); toast(t('exported')); } function clearAllData() { if (!confirm(t('confirm_clear'))) return; ['tasks','routines','projects','library','profile'].forEach(k => localStorage.removeItem('ffos_'+k)); render(); toast(t('data_cleared')); } // ═══════════════════════════════════════════ // THEME // ═══════════════════════════════════════════ function setTheme(t) { document.documentElement.setAttribute('data-theme', t === 'dark' ? 'dark' : ''); localStorage.setItem('ffos_theme', t); updateThemeBtns(); toast(t === 'dark' ? '🌙 Tema escuro' : '☀️ Tema claro'); } function toggleTheme() { setTheme((localStorage.getItem('ffos_theme')||'light') === 'dark' ? 'light' : 'dark'); } function updateThemeBtns() { const t = localStorage.getItem('ffos_theme') || 'light'; const l = document.getElementById('btn-theme-light'); const d = document.getElementById('btn-theme-dark'); if (l) l.className = 'settings-btn'+(t==='light'?' active':''); if (d) d.className = 'settings-btn'+(t==='dark'?' active':''); const btn = document.getElementById('theme-btn'); if (btn) btn.textContent = t === 'dark' ? '☀' : '◐'; } // ═══════════════════════════════════════════ // LANG // ═══════════════════════════════════════════ let lang = localStorage.getItem('ffos_lang') || 'pt'; // ── TRANSLATIONS ── var TX = { pt: { nav_today:'Hoje', nav_tasks:'Tarefas', nav_routines:'Rotinas', nav_projects:'Projetos', nav_lib:'Biblioteca', nav_settings:'Configurações', sb_capture:'CAPTURAR', sb_voice:'VOZ RÁPIDA', sb_search:'PESQUISAR', sb_ask:'PERGUNTAR', sb_logout:'SAIR', today_top3:'TOP 3 DO DIA', today_upnext:'EM SEGUIDA', today_viewall:'Ver tudo →', today_alltasks:'TODAS ABERTAS', today_addtask:'+ Nova tarefa', today_slipping:'ESCORREGANDO', today_routines:'ROTINAS', today_resurf:'RELEMBRAR', today_review:'REVISAR', today_no_events:'Nenhum evento hoje.', today_no_tasks:'Nenhuma tarefa para hoje.', today_no_slip:'Nenhuma tarefa esquecida.', today_no_routine:'Nenhuma rotina cadastrada.', today_no_review:'Nada para revisar.', today_top3_empty:'⭐ Marque uma tarefa com estrela para definir o top 3.', today_vaga:'(vaga aberta)', today_focus:'⊙ Foco', today_focus_exit:'✕ Sair do foco', tasks_title:'Tarefas', tasks_new:'+ Nova tarefa', tasks_all:'Todas', tasks_open:'Abertas', tasks_done:'Concluídas', tasks_overdue:'Atrasadas', tasks_none:'Nenhuma tarefa.', routines_title:'Rotinas', routines_new:'+ Nova rotina', routines_none:'Nenhuma rotina.', period_manha:'☀️ Manhã', period_tarde:'🌤 Tarde', period_noite:'🌙 Noite', projects_title:'Projetos', projects_new:'+ Novo projeto', projects_none:'Nenhum projeto.', lib_title:'Biblioteca', lib_add:'+ Adicionar', lib_notes:'Notas', lib_quotes:'Citações', lib_books:'Livros', lib_none:'Nenhum item.', settings_title:'Configurações', cfg_profile:'PERFIL', cfg_name:'Nome', cfg_role:'Cargo', cfg_account:'Conta Google', cfg_logout:'Sair', cfg_appearance:'APARÊNCIA', cfg_theme:'Tema', cfg_theme_light:'☀️ Claro', cfg_theme_dark:'🌙 Escuro', cfg_colors:'Cores de categoria', cfg_lang:'IDIOMA', cfg_lang_label:'Idioma do app', cfg_gcal:'GOOGLE CALENDAR', cfg_gcal_sub:'Eventos aparecem em "Em Seguida"', cfg_gcal_connect:'🔗 Conectar', cfg_gcal_disconnect:'✕ Desconectar', cfg_gcal_on:'✓ Conectado', cfg_gcal_off:'Não conectado', cfg_data:'DADOS', cfg_export:'Exportar', cfg_export_sub:'Backup JSON de todos os dados', cfg_clear:'Limpar tudo', cfg_clear_sub:'Remove todos os dados permanentemente', modal_task_new:'Nova Tarefa', modal_task_edit:'Editar Tarefa', modal_task_name:'Nome', modal_task_date:'Data', modal_task_time:'Horário', modal_task_cat:'Categoria', modal_task_notes:'Notas', modal_task_ph:'O que precisa ser feito?', modal_task_notes_ph:'Notas adicionais...', modal_task_del:'Excluir', modal_cancel:'Cancelar', modal_save:'Salvar', cat_none:'— Sem categoria —', cat_aula:'🟣 Aulas', cat_admin:'🔵 Admin', cat_cobranca:'🟡 Cobranças', cat_pessoal:'🩷 Pessoal', modal_routine_new:'Nova Rotina', modal_routine_edit:'Editar Rotina', modal_routine_name:'Nome', modal_routine_period:'Período', modal_routine_goal:'Meta de dias (streak)', modal_proj_new:'Novo Projeto', modal_proj_edit:'Editar Projeto', modal_proj_name:'Nome', modal_proj_desc:'Descrição', modal_proj_deadline:'Prazo', modal_proj_status:'Status', status_ativo:'Ativo', status_pausado:'Pausado', status_concluido:'Concluído', modal_lib_new:'Adicionar', modal_lib_edit:'Editar', modal_lib_title:'Título', modal_lib_body:'Conteúdo', modal_lib_tags:'Tags', modal_lib_review:'Marcar para revisão', modal_lib_yes:'Sim', modal_lib_no:'Não', ask_placeholder:'Pergunte sobre suas tarefas...', ask_welcome:'Olá! Posso responder perguntas sobre suas tarefas e rotinas.', search_placeholder:'Pesquisar tarefas, rotinas, projetos...', search_none:'Nenhum resultado.', gcal_connect_prompt:'Conectar Google Calendar para ver eventos', streak_days:'dias · meta:', reviewed:'Marcado como revisado!', saved:'Salvo!', deleted:'Excluído!', confirm_delete_task:'Excluir esta tarefa?', confirm_delete_routine:'Excluir esta rotina?', confirm_delete_project:'Excluir este projeto?', confirm_delete_lib:'Excluir?', confirm_clear:'Tem certeza? Isso apaga TODOS os dados permanentemente.', exported:'Exportado!', data_cleared:'Dados limpos.', profile_saved:'Perfil salvo!', mic_listening:'🔴 Ouvindo...', mic_no_browser:'Reconhecimento de voz não disponível. Use o Chrome.', mic_no_permission:'❌ Permita o acesso ao microfone nas configurações do Chrome.', days:['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'], months:['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], short_days:['Dom','Seg','Ter','Qua','Qui','Sex','Sab'] }, en: { nav_today:'Today', nav_tasks:'Tasks', nav_routines:'Routines', nav_projects:'Projects', nav_lib:'Library', nav_settings:'Settings', sb_capture:'CAPTURE', sb_voice:'QUICK VOICE', sb_search:'SEARCH', sb_ask:'ASK', sb_logout:'SIGN OUT', today_top3:'TOP 3 TODAY', today_upnext:'UP NEXT', today_viewall:'View all →', today_alltasks:'ALL OPEN', today_addtask:'+ New task', today_slipping:'SLIPPING', today_routines:'ROUTINES', today_resurf:'RESURFACING', today_review:'NEEDS REVIEW', today_no_events:'No events today.', today_no_tasks:t('today_no_tasks'), today_no_slip:t('today_no_slip'), today_no_routine:t('today_no_routine'), today_no_review:t('today_no_review'), today_vaga:'(open slot)', today_top3_empty:'⭐ Star a task below to set your top 3.', today_vaga:t('today_vaga'), today_focus:'⊙ Focus', today_focus_exit:'✕ Exit focus', tasks_title:'Tasks', tasks_new:'+ New task', tasks_all:'All', tasks_open:'Open', tasks_done:'Done', tasks_overdue:'Overdue', tasks_none:t('tasks_none'), routines_title:'Routines', routines_new:'+ New routine', routines_none:t('routines_none'), period_manha:'☀️ Morning', period_tarde:'🌤 Afternoon', period_noite:'🌙 Evening', projects_title:'Projects', projects_new:'+ New project', projects_none:t('projects_none'), lib_title:'Library', lib_add:'+ Add', lib_notes:'Notes', lib_quotes:'Quotes', lib_books:'Books', lib_none:t('lib_none'), settings_title:'Settings', cfg_profile:'PROFILE', cfg_name:'Name', cfg_role:'Role', cfg_account:'Google Account', cfg_logout:'Sign out', cfg_appearance:'APPEARANCE', cfg_theme:'Theme', cfg_theme_light:'☀️ Light', cfg_theme_dark:'🌙 Dark', cfg_colors:'Category colors', cfg_lang:'LANGUAGE', cfg_lang_label:'App language', cfg_gcal:'GOOGLE CALENDAR', cfg_gcal_sub:'Events appear in "Up Next"', cfg_gcal_connect:'🔗 Connect', cfg_gcal_disconnect:'✕ Disconnect', cfg_gcal_on:'✓ Connected', cfg_gcal_off:'Not connected', cfg_data:'DATA', cfg_export:'Export', cfg_export_sub:'JSON backup of all data', cfg_clear:'Clear all', cfg_clear_sub:'Permanently removes all data', modal_task_new:'New Task', modal_task_edit:'Edit Task', modal_task_name:'Name', modal_task_date:'Date', modal_task_time:'Time', modal_task_cat:'Category', modal_task_notes:'Notes', modal_task_ph:'What needs to be done?', modal_task_notes_ph:'Additional notes...', modal_task_del:'Delete', modal_cancel:'Cancel', modal_save:'Save', cat_none:'— No category —', cat_aula:'🟣 Classes', cat_admin:'🔵 Admin', cat_cobranca:'🟡 Billing', cat_pessoal:'🩷 Personal', modal_routine_new:'New Routine', modal_routine_edit:'Edit Routine', modal_routine_name:'Name', modal_routine_period:'Period', modal_routine_goal:'Streak goal (days)', modal_proj_new:'New Project', modal_proj_edit:'Edit Project', modal_proj_name:'Name', modal_proj_desc:'Description', modal_proj_deadline:'Deadline', modal_proj_status:'Status', status_ativo:'Active', status_pausado:'Paused', status_concluido:'Completed', modal_lib_new:'Add', modal_lib_edit:'Edit', modal_lib_title:'Title', modal_lib_body:'Content', modal_lib_tags:'Tags', modal_lib_review:'Flag for review', modal_lib_yes:'Yes', modal_lib_no:'No', ask_placeholder:'Ask about your tasks...', ask_welcome:'Hi! I can answer questions about your tasks and routines.', search_placeholder:'Search tasks, routines, projects...', search_none:'No results.', gcal_connect_prompt:'Connect Google Calendar to see events', streak_days:'days · goal:', reviewed:'Marked as reviewed!', saved:'Saved!', deleted:'Deleted!', confirm_delete_task:'Delete this task?', confirm_delete_routine:'Delete this routine?', confirm_delete_project:'Delete this project?', confirm_delete_lib:'Delete?', confirm_clear:'Are you sure? This permanently deletes ALL data.', exported:'Exported!', data_cleared:'Data cleared.', profile_saved:'Profile saved!', mic_listening:'🔴 Listening...', mic_no_browser:'Voice recognition not available. Use Chrome.', mic_no_permission:'❌ Please allow microphone access in Chrome settings.', days:['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], months:['January','February','March','April','May','June','July','August','September','October','November','December'], short_days:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] } }; function t(key) { try { if (!window.TX) return key; var l = lang || 'pt'; return (TX[l] && TX[l][key]) || (TX['pt'] && TX['pt'][key]) || key; } catch(e) { return key; } } function setLang(l) { lang = l; localStorage.setItem('ffos_lang', l); updateLangBtns(); applyTranslations(); render(); } function applyTranslations() { // Sidebar nav var navItems = document.querySelectorAll('.nav-item'); var navKeys = ['nav_today','nav_tasks','nav_routines','nav_projects','nav_lib','nav_settings']; navItems.forEach(function(el, i) { var badge = el.querySelector('.nav-badge'); el.textContent = t(navKeys[i]); if (badge) el.appendChild(badge); }); // Sidebar footer var footBtns = document.querySelectorAll('.sb-foot-btn span:first-child'); var footKeys = ['sb_capture','sb_voice','sb_search','sb_ask']; footBtns.forEach(function(el, i) { if (footKeys[i] && !el.closest('.danger')) el.textContent = t(footKeys[i]); }); // Search placeholder var si = document.getElementById('search-input'); if (si) si.placeholder = t('search_placeholder'); // Ask placeholder var ai = document.getElementById('ask-input'); if (ai) ai.placeholder = t('ask_placeholder'); // Task modal fields setElText('mo-task-name-lbl', t('modal_task_name')); setElText('mo-task-date-lbl', t('modal_task_date')); setElText('mo-task-time-lbl', t('modal_task_time')); setElText('mo-task-cat-lbl', t('modal_task_cat')); setElText('mo-task-notes-lbl', t('modal_task_notes')); setElPlaceholder('t-name', t('modal_task_ph')); setElPlaceholder('t-notes', t('modal_task_notes_ph')); // Category options var catEl = document.getElementById('t-cat'); if (catEl) catEl.innerHTML = '' +'' +'' +'' +''; } function setElText(id, val) { var el=document.getElementById(id); if(el) el.textContent=val; } function setElPlaceholder(id, val) { var el=document.getElementById(id); if(el) el.placeholder=val; } function updateLangBtns() { const pt = document.getElementById('btn-lang-pt'); const en = document.getElementById('btn-lang-en'); if (pt) pt.className = 'settings-btn'+(lang==='pt'?' active':''); if (en) en.className = 'settings-btn'+(lang==='en'?' active':''); } // ═══════════════════════════════════════════ // CATEGORY COLORS // ═══════════════════════════════════════════ function setCatColor(cat, color) { document.documentElement.style.setProperty('--cat-'+cat, color); const saved = JSON.parse(localStorage.getItem('ffos_cat_colors')||'{}'); saved[cat] = color; localStorage.setItem('ffos_cat_colors', JSON.stringify(saved)); } function applyCatColors() { const saved = JSON.parse(localStorage.getItem('ffos_cat_colors')||'{}'); Object.entries(saved).forEach(([k,v]) => document.documentElement.style.setProperty('--cat-'+k, v)); } function updateCatColorInputs() { const saved = JSON.parse(localStorage.getItem('ffos_cat_colors')||'{}'); const defaults = { aula:'#7c3aed', admin:'#1d4ed8', cobranca:'#b45309', pessoal:'#be185d' }; Object.entries(defaults).forEach(([k,v]) => { const el = document.getElementById('color-'+k); if (el) el.value = saved[k] || v; }); } // ═══════════════════════════════════════════ // FOCUS MODE // ═══════════════════════════════════════════ let _focus = false; function toggleFocus() { _focus = !_focus; document.body.classList.toggle('focus', _focus); const btn = document.getElementById('focus-btn'); if (btn) btn.textContent = _focus ? '✕ Sair do foco' : '⊙ Foco'; } // ═══════════════════════════════════════════ // ASK // ═══════════════════════════════════════════ var _askMsgs = []; function openAsk() { openMo('mo-ask'); if (!_askMsgs.length) { _askMsgs = [{role:'assistant',text:t('ask_welcome')}]; renderAskMsgs(); } setTimeout(function(){ var el=document.getElementById('ask-input'); if(el){el.value='';el.focus();} }, 100); } function renderAskMsgs() { var el = document.getElementById('ask-msgs'); if (!el) return; el.innerHTML = _askMsgs.map(function(m) { return '
'+m.text+'
'; }).join(''); el.scrollTop = el.scrollHeight; } function sendAsk() { var inp = document.getElementById('ask-input'); var q = inp ? inp.value.trim() : ''; if (!q) return; if (inp) inp.value = ''; _askMsgs.push({role:'user', text:q}); _askMsgs.push({role:'assistant', text:'Pensando...'}); renderAskMsgs(); var todayStr = dk(today0()); var tasks = getTasks(); var open = tasks.filter(function(t){return !t.done;}).map(function(t){return t.name;}).join(', '); var overdue = tasks.filter(function(t){return !t.done&&t.date&&t.date]+>/g,'')};}) ) }) }).then(function(r){return r.json();}).then(function(d){ var txt = d.choices&&d.choices[0] ? d.choices[0].message.content : 'Sem resposta.'; if(d.error) txt = 'Erro: '+(d.error.message||'tente novamente.'); _askMsgs[_askMsgs.length-1] = {role:'assistant', text: txt}; renderAskMsgs(); }).catch(function(){ _askMsgs[_askMsgs.length-1] = {role:'assistant', text:'Erro ao conectar.'}; renderAskMsgs(); }); } // ═══════════════════════════════════════════ // MOBILE NAV // ═══════════════════════════════════════════ function toggleSidebar() { var sb = document.getElementById('sidebar'); var ov = document.getElementById('sidebar-overlay'); if (!sb) return; var isOpen = sb.classList.contains('open'); sb.classList.toggle('open', !isOpen); if (ov) ov.classList.toggle('show', !isOpen); } function closeSidebar() { var sb = document.getElementById('sidebar'); var ov = document.getElementById('sidebar-overlay'); if (sb) sb.classList.remove('open'); if (ov) ov.classList.remove('show'); } function toggleMobMore() { var menu = document.getElementById('mob-more-menu'); if (menu) menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; } function closeMobMore() { var menu = document.getElementById('mob-more-menu'); if (menu) menu.style.display = 'none'; } function updateMobNav(page) { var pages = ['today','tasks','routines','projects']; pages.forEach(function(p) { var btn = document.getElementById('mob-btn-'+p); if (btn) btn.classList.toggle('active', p === page); }); var searchBtn = document.getElementById('mob-btn-search'); if (searchBtn) searchBtn.classList.remove('active'); } // Render mobile sections (right panel shown inline on mobile) function renderMobileSections() { var todayStr = dk(today0()); // Slipping var slipMob = document.getElementById('slipping-list-mob'); if (slipMob) { var slipping = getTasks().filter(function(task) { if (task.done || !task.updated) return false; return Math.floor((new Date(todayStr) - new Date(task.updated)) / 86400000) >= 3; }).slice(0,5); if (!slipping.length) { slipMob.innerHTML = '
'+t('today_no_slip')+'
'; } else { slipMob.innerHTML = slipping.map(function(task) { var days = Math.floor((new Date(todayStr) - new Date(task.updated)) / 86400000); return '
'+escHtml(task.name.substring(0,35))+'
'+days+'d
'; }).join(''); } } // Routines var rtMob = document.getElementById('routines-today-mob'); if (rtMob) { var routines = getRoutines(); if (!routines.length) { rtMob.innerHTML = '
'+t('today_no_routine')+'
'; } else { var periods = [{key:'manha',label:t('period_manha')},{key:'tarde',label:t('period_tarde')},{key:'noite',label:t('period_noite')}]; var html2 = ''; periods.forEach(function(p) { var pr = routines.filter(function(r) { return r.period === p.key; }); if (!pr.length) return; html2 += '
'+p.label+'
'; pr.forEach(function(r) { var done = (r.completions||[]).includes(todayStr); var cc = done ? 'done' : ''; html2 += '
' + '
' + '
'+escHtml(r.name)+'
' + '
'+streak7(r,todayStr)+'
' + '
'; }); }); rtMob.innerHTML = html2; } } // Resurfacing var resurf = getLib().filter(function(l) { return l.body && l.body.length > 20; }); var resurfMob = document.getElementById('resurf-content-mob'); var resurfSec = document.getElementById('resurf-sec-mob'); if (resurfMob) { if (!resurf.length) { if (resurfSec) resurfSec.style.display = 'none'; } else { if (resurfSec) resurfSec.style.display = 'block'; var item = resurf[Math.floor(new Date().getDate() % resurf.length)]; resurfMob.innerHTML = '
"'+escHtml(item.body.substring(0,200))+'"
' + '
— '+(item.title||'Biblioteca')+'
'; } } // Review var reviewMob = document.getElementById('review-list-mob'); var reviewCntMob = document.getElementById('review-count-mob'); if (reviewMob) { var reviews = getLib().filter(function(l) { return l.review; }); if (reviewCntMob) reviewCntMob.textContent = reviews.length ? '· '+reviews.length : ''; if (!reviews.length) { reviewMob.innerHTML = '
'+t('today_no_review')+'
'; } else { reviewMob.innerHTML = reviews.map(function(l) { return '
' + '
'+escHtml((l.title||l.body||'').substring(0,80))+'
' + '
Marcar como revisado ✓
' + '
'; }).join(''); } } } function escHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ═══════════════════════════════════════════ // GROQ WHISPER TRANSCRIPTION // ═══════════════════════════════════════════ function saveGroqKey() { var key = document.getElementById('cfg-groq-key').value.trim(); if (key) { localStorage.setItem('ffos_groq_key', key); updateGroqStatus(); toast('✓ Groq API Key salva!'); } } function updateGroqStatus() { var key = localStorage.getItem('ffos_groq_key'); var el = document.getElementById('groq-status'); var cfgEl = document.getElementById('cfg-groq-key'); if (cfgEl && key) cfgEl.value = key.substring(0,8)+'...'; if (el) { el.textContent = key ? '✓ Groq ativo — Whisper (voz) + Llama 3.3 (chat IA)' : 'Sem Groq Key — voz básica, chat IA desativado'; el.style.color = key ? 'var(--green)' : 'var(--text3)'; } } // Record audio blob and transcribe with Groq Whisper var _mediaRecorder = null; var _audioChunks = []; var _useGroq = false; function startGroqRecording(onResult, onError) { var groqKey = localStorage.getItem('ffos_groq_key'); if (!groqKey) return false; // Fall back to Web Speech navigator.mediaDevices.getUserMedia({ audio: true }) .then(function(stream) { _audioChunks = []; _mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); _mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) _audioChunks.push(e.data); }; _mediaRecorder.onstop = function() { stream.getTracks().forEach(function(t) { t.stop(); }); var blob = new Blob(_audioChunks, { type: 'audio/webm' }); transcribeWithGroq(blob, groqKey, onResult, onError); }; _mediaRecorder.start(); _useGroq = true; return true; }) .catch(function(err) { console.warn('Microphone error:', err); onError && onError(err.message); }); return true; } function stopGroqRecording() { if (_mediaRecorder && _mediaRecorder.state !== 'inactive') { _mediaRecorder.stop(); } } function transcribeWithGroq(audioBlob, apiKey, onResult, onError) { var formData = new FormData(); formData.append('file', audioBlob, 'audio.webm'); formData.append('model', 'whisper-large-v3-turbo'); formData.append('language', lang === 'en' ? 'en' : 'pt'); formData.append('response_format', 'text'); fetch('https://api.groq.com/openai/v1/audio/transcriptions', { method: 'POST', headers: { 'Authorization': 'Bearer ' + apiKey }, body: formData }) .then(function(r) { return r.text(); }) .then(function(text) { onResult && onResult(text.trim()); }) .catch(function(err) { console.warn('Groq transcription error:', err); onError && onError(err.message); }); } // ═══════════════════════════════════════════ // VOICE DIARY // ═══════════════════════════════════════════ var _vdType = 'task'; var _vdTranscript = ''; var _vdRec = null; var _vdRecording = false; function setVdType(type) { _vdType = type; document.querySelectorAll('.vd-type-btn').forEach(function(b) { b.classList.remove('active'); }); var btn = document.getElementById('vd-type-'+type); if (btn) btn.classList.add('active'); } // Voice capture state var _vdConversation = []; // [{role:'ai'|'user', text:'...'}] var _vdTaskData = {}; // accumulated task fields var _vdStep = 0; // conversation step function openVoiceDiary() { _vdTranscript = ''; _vdType = 'task'; _vdRecording = false; _vdParsed = null; window._vdParsed = null; // Reset UI to listen state showVdState('listen'); var transcript = document.getElementById('vd-transcript'); var hint = document.getElementById('vd-hint'); var wave = document.getElementById('vd-wave'); var bigMic = document.getElementById('vd-big-mic'); if (transcript) { transcript.textContent = ''; transcript.style.display = 'none'; } if (hint) hint.textContent = 'Toque no microfone para começar'; if (wave) wave.style.display = 'none'; if (bigMic) bigMic.style.background = 'var(--accent)'; document.getElementById('vd-state-label').textContent = 'Ouvindo...'; openMo('mo-voice-diary'); } function showVdState(state) { // states: 'listen', 'processing', 'preview' var listen = document.getElementById('vd-listen-state'); var processing = document.getElementById('vd-processing-state'); var preview = document.getElementById('vd-preview-state'); if (listen) listen.style.display = state === 'listen' ? 'block' : 'none'; if (processing) processing.style.display = state === 'processing' ? 'block' : 'none'; if (preview) preview.style.display = state === 'preview' ? 'block' : 'none'; } function resetVoiceDiary() { _vdTranscript = ''; window._vdParsed = null; showVdState('listen'); var transcript = document.getElementById('vd-transcript'); var hint = document.getElementById('vd-hint'); var wave = document.getElementById('vd-wave'); var bigMic = document.getElementById('vd-big-mic'); if (transcript) { transcript.textContent = ''; transcript.style.display = 'none'; } if (hint) hint.textContent = 'Toque no microfone para começar'; if (wave) wave.style.display = 'none'; if (bigMic) bigMic.style.background = 'var(--accent)'; document.getElementById('vd-state-label').textContent = 'Ouvindo...'; } function toggleVoiceDiaryMic() { if (_vdRecording) { // Stop recording if (_useGroq) stopGroqRecording(); else if (_vdRec) _vdRec.stop(); return; } var wave = document.getElementById('vd-wave'); var transcript = document.getElementById('vd-transcript'); var hint = document.getElementById('vd-hint'); var bigMic = document.getElementById('vd-big-mic'); var label = document.getElementById('vd-state-label'); var fab = document.getElementById('fab-mic'); _vdRecording = true; _vdTranscript = ''; _useGroq = false; if (wave) wave.style.display = 'flex'; if (transcript) { transcript.style.display = 'none'; transcript.textContent = ''; } if (hint) hint.textContent = 'Toque novamente para parar'; if (bigMic) bigMic.style.background = 'var(--red)'; if (label) label.textContent = 'Gravando...'; if (fab) fab.classList.add('recording'); // Try Groq Whisper first var groqKey = localStorage.getItem('ffos_groq_key'); if (groqKey) { startGroqRecording( function(text) { // Got transcription _vdTranscript = text; _vdRecording = false; if (fab) fab.classList.remove('recording'); if (wave) wave.style.display = 'none'; if (text.trim()) { if (transcript) { transcript.textContent = text; transcript.style.display = 'block'; } if (hint) hint.textContent = ''; if (label) label.textContent = 'Transcrição concluída'; if (bigMic) bigMic.style.background = 'var(--accent)'; setTimeout(function() { processVoiceWithAI(text); }, 600); } else { if (bigMic) bigMic.style.background = 'var(--accent)'; if (label) label.textContent = 'Nenhuma fala detectada'; if (hint) hint.textContent = 'Tente novamente'; } }, function() { _vdRecording = false; if (fab) fab.classList.remove('recording'); useFallbackSpeech(wave, transcript, hint, bigMic, label, fab); } ); } else { useFallbackSpeech(wave, transcript, hint, bigMic, label, fab); } } function useFallbackSpeech(wave, transcript, hint, bigMic, label, fab) { var SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) { toast('Use o Chrome para reconhecimento de voz.'); _vdRecording = false; if (bigMic) bigMic.style.background = 'var(--accent)'; return; } _vdRec = new SR(); _vdRec.lang = lang === 'en' ? 'en-US' : 'pt-BR'; _vdRec.continuous = false; _vdRec.interimResults = true; _vdRec.onresult = function(e) { var interim = '', final = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) final += e.results[i][0].transcript; else interim += e.results[i][0].transcript; } _vdTranscript += final; if (transcript) { transcript.textContent = _vdTranscript + interim; transcript.style.display = 'block'; } }; _vdRec.onend = function() { _vdRecording = false; if (wave) wave.style.display = 'none'; if (bigMic) bigMic.style.background = 'var(--accent)'; if (fab) fab.classList.remove('recording'); if (_vdTranscript.trim()) { if (label) label.textContent = 'Transcrição concluída'; if (hint) hint.textContent = ''; setTimeout(function() { processVoiceWithAI(_vdTranscript); }, 400); } else { if (label) label.textContent = 'Nenhuma fala detectada'; if (hint) hint.textContent = 'Tente novamente'; } }; _vdRec.onerror = function(e) { _vdRecording = false; if (wave) wave.style.display = 'none'; if (bigMic) bigMic.style.background = 'var(--accent)'; if (fab) fab.classList.remove('recording'); if (e.error !== 'aborted') toast('Erro: ' + e.error); if (label) label.textContent = 'Erro ao ouvir'; if (hint) hint.textContent = 'Tente novamente'; }; _vdRec.start(); } function processVoiceWithAI(text) { // Show processing state showVdState('processing'); var todayStr = dk(today0()); var groqKey = localStorage.getItem('ffos_groq_key'); var systemPrompt = 'Você recebe um texto ditado por voz sobre uma tarefa. ' + 'Extraia as informações e responda SOMENTE em JSON válido sem markdown. ' + 'Formato: {"name":"nome da tarefa","date":"YYYY-MM-DD ou vazio","time":"HH:MM ou vazio","cat":"aula|admin|cobranca|pessoal ou vazio","priority":"alta|media|baixa","notes":"notas extras ou vazio"}. ' + 'Hoje é ' + todayStr + '. ' + 'Interprete expressões como "amanhã", "sexta", "às 3 da tarde"=15:00, "meio-dia"=12:00. ' + 'Remova hesitações como "humm", "ãhh". ' + 'Para categoria: aula=aulas/turmas/alunos, admin=administrativo/escola, cobranca=pagamentos/mensalidades, pessoal=vida pessoal. ' + 'Responda APENAS JSON.'; if (!groqKey) { // No AI - just create task from raw text var parsed = { name: text.substring(0, 80), date: todayStr, time: '', cat: '', priority: 'media', notes: '' }; window._vdParsed = parsed; showTaskPreview(parsed); return; } fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + groqKey }, body: JSON.stringify({ model: 'llama-3.3-70b-versatile', max_tokens: 200, temperature: 0.1, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: text } ] }) }) .then(function(r) { return r.json(); }) .then(function(d) { if (d.error) throw new Error(d.error.message); var result = d.choices && d.choices[0] ? d.choices[0].message.content : ''; try { var clean = result.replace(/```json|```/g, '').trim(); var parsed = JSON.parse(clean); window._vdParsed = parsed; showTaskPreview(parsed); } catch(e) { // JSON parse failed - use raw text var parsed2 = { name: text.substring(0, 80), date: todayStr, time: '', cat: '', priority: 'media', notes: result }; window._vdParsed = parsed2; showTaskPreview(parsed2); } addRecentCapture('task', window._vdParsed.name || text); }) .catch(function(err) { console.warn('AI error:', err); // Fallback: create task from raw text var parsed3 = { name: text.substring(0, 80), date: todayStr, time: '', cat: '', priority: 'media', notes: '' }; window._vdParsed = parsed3; showTaskPreview(parsed3); }); } function showTaskPreview(parsed) { var el = document.getElementById('vd-preview-content'); if (!el) return; var catLabels = { aula:'🟣 Aulas', admin:'🔵 Admin', cobranca:'🟡 Cobranças', pessoal:'🩷 Pessoal' }; var priorityColor = { alta: 'var(--red)', media: 'var(--text3)', baixa: 'var(--green)' }; var pColor = priorityColor[parsed.priority] || 'var(--text3)'; el.innerHTML = '
' + escHtml(parsed.name || 'Nova tarefa') + '
' + '
' + (parsed.date ? '📅 ' + formatDatePretty(parsed.date) + '' : '') + (parsed.time ? '⏰ ' + parsed.time + '' : '') + (parsed.cat ? '' + (catLabels[parsed.cat]||parsed.cat) + '' : '') + (parsed.priority ? '● ' + parsed.priority + '' : '') + '
' + (parsed.notes ? '
' + escHtml(parsed.notes) + '
' : ''); showVdState('preview'); } function callClaude(system, messages, onSuccess) { var groqKey = localStorage.getItem('ffos_groq_key'); if (!groqKey) { addVdBubble('ai', 'Configure a Groq API Key em Configurações → Transcrição por Voz para usar a IA.'); return; } // Groq chat API (OpenAI-compatible, no CORS issues) var msgs = [{role:'system', content: system}].concat(messages); fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + groqKey }, body: JSON.stringify({ model: 'llama-3.3-70b-versatile', max_tokens: 400, messages: msgs }) }) .then(function(r) { return r.json(); }) .then(function(d) { var text = d.choices && d.choices[0] ? d.choices[0].message.content : ''; if (d.error) { console.warn('Groq error:', d.error); addVdBubble('ai', 'Erro na IA: ' + (d.error.message || 'tente novamente.')); return; } onSuccess(text); }) .catch(function(err) { addVdBubble('ai', 'Erro ao conectar com a IA.'); console.warn('Groq error:', err); }); } function updateTaskPreview(parsed) { var preview = document.getElementById('vd-preview'); var content = document.getElementById('vd-preview-content'); if (!preview || !content) return; var priorityColors = {alta:'var(--red)', media:'#f59e0b', baixa:'var(--green)'}; var catLabels = {aula:'🟣 Aulas', admin:'🔵 Admin', cobranca:'🟡 Cobranças', pessoal:'🩷 Pessoal'}; content.innerHTML = '
' + escHtml(parsed.name || '') + '
' + '
' + (parsed.date ? '📅 '+formatDatePretty(parsed.date)+'' : '') + (parsed.time ? '⏰ '+parsed.time+'' : '') + (parsed.cat ? ''+( catLabels[parsed.cat]||parsed.cat)+'' : '') + (parsed.priority ? '● '+parsed.priority.charAt(0).toUpperCase()+parsed.priority.slice(1)+'' : '') + '
' + (parsed.notes ? '
📝 '+escHtml(parsed.notes)+'
' : ''); preview.style.display = 'block'; } function formatDatePretty(dateStr) { if (!dateStr) return ''; var d = new Date(dateStr + 'T12:00:00'); var days = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb']; var months = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez']; return days[d.getDay()] + ', ' + d.getDate() + ' ' + months[d.getMonth()]; } function saveVoiceCapture() { var parsed = window._vdParsed || {}; var tasks = getTasks(); tasks.push({ id: uid(), name: parsed.name || _vdTranscript.substring(0, 60) || 'Nova tarefa', date: parsed.date || dk(today0()), time: parsed.time || '', cat: parsed.cat || '', notes: parsed.notes || '', done: false, starred: parsed.priority === 'alta', created: Date.now(), updated: dk(today0()) }); setTasks(tasks); closeVoiceDiary(); render(); var msg = '✓ Tarefa criada' + (parsed.priority === 'alta' ? ' — adicionada ao Top 3!' : '!'); toast(msg); addNotification('✓', 'Tarefa criada por voz', parsed.name || 'Nova tarefa'); } // ═══════════════════════════════════════════ // RECENT CAPTURES // ═══════════════════════════════════════════ function addRecentCapture(type, text) { var captures = JSON.parse(localStorage.getItem('ffos_captures') || '[]'); captures.unshift({ type: type, text: text.substring(0, 100), time: new Date().toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'}), date: dk(today0()) }); if (captures.length > 20) captures = captures.slice(0, 20); localStorage.setItem('ffos_captures', JSON.stringify(captures)); } // ═══════════════════════════════════════════ // PUSH NOTIFICATIONS (Web Push API) // ═══════════════════════════════════════════ var _pushSupported = 'Notification' in window && 'serviceWorker' in navigator; function requestPushPermission() { if (!_pushSupported) return; if (Notification.permission === 'default') { Notification.requestPermission().then(function(perm) { if (perm === 'granted') { toast('🔔 Notificações ativadas!'); schedulePushChecks(); } }); } else if (Notification.permission === 'granted') { schedulePushChecks(); } } function showPushNotification(title, body) { if (Notification.permission === 'granted') { try { new Notification(title, { body: body, icon: '/icon.png', badge: '/icon.png', tag: 'ffos-' + Date.now() }); } catch(e) { // Fallback: in-app banner showPushBanner(title + ' — ' + body); } } else { showPushBanner(title + ' — ' + body); } } function showPushBanner(msg) { var banner = document.getElementById('push-banner'); var text = document.getElementById('push-banner-text'); if (!banner || !text) return; text.textContent = msg; banner.classList.add('show'); setTimeout(function() { banner.classList.remove('show'); }, 8000); } function checkDueItems() { var now = new Date(); var todayStr = dk(today0()); var nowTime = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0'); // Check tasks due now getTasks().forEach(function(t) { if (!t.done && t.date === todayStr && t.time === nowTime) { showPushNotification('⏰ Tarefa agora!', t.name); } }); // Check overdue tasks (once per day at 9am) if (nowTime === '09:00') { var overdue = getTasks().filter(function(t) { return !t.done && t.date && t.date < todayStr; }); if (overdue.length > 0) { showPushNotification('⚠️ ' + overdue.length + ' tarefa(s) atrasada(s)', overdue.slice(0,3).map(function(t){return t.name;}).join(', ')); } } // Check routines — morning at 7am, afternoon at 12pm, evening at 7pm var routineAlerts = [{time:'07:00',period:'manha'},{time:'12:00',period:'tarde'},{time:'19:00',period:'noite'}]; routineAlerts.forEach(function(alert) { if (nowTime === alert.time) { var pending = getRoutines().filter(function(r) { return r.period === alert.period && !(r.completions||[]).includes(todayStr); }); if (pending.length > 0) { var periodName = {manha:'Manhã',tarde:'Tarde',noite:'Noite'}[alert.period]; showPushNotification('🔁 Rotinas de ' + periodName, pending.map(function(r){return r.name;}).join(', ')); } } }); } function schedulePushChecks() { // Check every minute setInterval(checkDueItems, 60000); // Check immediately checkDueItems(); } // ═══════════════════════════════════════════ // SPEECH RECOGNITION // ═══════════════════════════════════════════ var _recognition = null; var _activeMicBtn = null; var _activeMicStatus = null; var _activeTargetId = null; function initSpeech() { var SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) { toast('Seu navegador não suporta reconhecimento de voz.'); return null; } var rec = new SR(); rec.lang = 'pt-BR'; rec.continuous = false; rec.interimResults = true; return rec; } function toggleMic(targetId, btnId, statusId) { // If already recording, stop if (_recognition && _activeMicBtn === btnId) { _recognition.stop(); return; } // Stop any existing recognition if (_recognition) { _recognition.stop(); } var SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) { toast(t('mic_no_browser')); return; } _recognition = new SR(); _recognition.lang = 'pt-BR'; _recognition.continuous = false; _recognition.interimResults = true; _activeMicBtn = btnId; _activeMicStatus = statusId; _activeTargetId = targetId; var btn = document.getElementById(btnId); var status = document.getElementById(statusId); var target = document.getElementById(targetId); if (btn) btn.classList.add('recording'); if (status) { status.textContent = t('mic_listening'); status.classList.add('show'); } // Save original value to append var originalVal = target ? (target.value || '') : ''; _recognition.onresult = function(e) { var interim = ''; var final = ''; for (var i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) { final += e.results[i][0].transcript; } else { interim += e.results[i][0].transcript; } } if (target) { target.value = originalVal + (originalVal ? ' ' : '') + (final || interim); } }; _recognition.onend = function() { if (btn) btn.classList.remove('recording'); if (status) { status.textContent = ''; status.classList.remove('show'); } _recognition = null; _activeMicBtn = null; }; _recognition.onerror = function(e) { if (btn) btn.classList.remove('recording'); if (status) { status.classList.remove('show'); } _recognition = null; _activeMicBtn = null; if (e.error === 'not-allowed') { toast(t('mic_no_permission')); } else if (e.error !== 'aborted') { toast('Erro no microfone: ' + e.error); } }; _recognition.start(); } // Quick voice capture — opens task modal and starts mic immediately function openVoiceCapture() { openVoiceDiary(); } // ═══════════════════════════════════════════ // SEARCH // ═══════════════════════════════════════════ function openSearch() { openGlobalSearch(); return; // old search below (unused): openMo('mo-search'); setTimeout(() => { const el = document.getElementById('search-input'); if(el){el.value='';el.focus();} }, 50); doSearch(''); } function doSearch(q) { const el = document.getElementById('search-results'); if (!el) return; const all = [ ...getTasks().map(x => ({type:'task', name:x.name, sub:x.cat||'tarefa', id:x.id, done:x.done})), ...getRoutines().map(x => ({type:'routine', name:x.name, sub:x.period||'rotina', id:x.id})), ...getProjects().map(x => ({type:'project', name:x.name, sub:'projeto', id:x.id})), ...getLib().map(x => ({type:'lib', name:x.title||x.body||'', sub:'biblioteca', id:x.id})) ]; const hits = q.trim() ? all.filter(x => x.name.toLowerCase().includes(q.toLowerCase())) : all.slice(0,10); if (!hits.length) { el.innerHTML = '
Nenhum resultado.
'; return; } el.innerHTML = hits.map(x => `
${x.name.substring(0,60)}
${x.sub}
`).join(''); } function searchGo(type, id) { closeMo('mo-search'); if (type === 'task') { openEditTask(id); } else if (type === 'routine') { goTo('routines'); } else if (type === 'project') { goTo('projects'); } else if (type === 'lib') { openEditLib(id); } } // ═══════════════════════════════════════════ // CAPTURE (quick task) // ═══════════════════════════════════════════ function openCapture() { openAddTask(); } // ═══════════════════════════════════════════ // GOOGLE CALENDAR // ═══════════════════════════════════════════ function gcalSilentReconnect() { if (!window._auth || !window._auth.currentUser) return; var provider = new firebase.auth.GoogleAuthProvider(); provider.addScope('https://www.googleapis.com/auth/calendar.readonly'); // Use silent re-auth - if user is already signed in this is seamless window._auth.currentUser.getIdToken(true) .then(function() { // Re-auth with popup to get fresh Calendar token return window._auth.signInWithPopup(provider); }) .then(function(result) { var token = result.credential ? result.credential.accessToken : null; if (token) { localStorage.setItem('ffos_gcal_token', token); localStorage.setItem('ffos_gcal_connected', '1'); gcalFetch(); // retry console.log('GCal token refreshed silently'); } }) .catch(function(err) { // Silent failed - show reconnect prompt localStorage.setItem('ffos_gcal_connected', '0'); gcalUpdateUI(); renderGCalPrompt(); console.warn('GCal silent reconnect failed:', err.code); }); } // Auto-refresh token every 45 minutes (token lasts ~60 min) function startGCalTokenRefresh() { setInterval(function() { if (localStorage.getItem('ffos_gcal_connected') === '1') { console.log('GCal: auto-refreshing token...'); gcalSilentReconnect(); } }, 45 * 60 * 1000); } function gcalConnect() { if (!window._auth) { toast('Aguarde o carregamento...'); return; } var provider = new firebase.auth.GoogleAuthProvider(); provider.addScope('https://www.googleapis.com/auth/calendar.readonly'); provider.setCustomParameters({ prompt: 'consent' }); window._auth.signInWithPopup(provider) .then(function(result) { var token = result.credential ? result.credential.accessToken : null; if (token) { localStorage.setItem('ffos_gcal_token', token); localStorage.setItem('ffos_gcal_connected', '1'); gcalFetch(); gcalUpdateUI(); toast('✅ Google Calendar conectado!'); } else { toast('Sem token. Tente novamente.'); } }) .catch(function(err) { console.error('gcalConnect:', err.code, err.message); if (err.code === 'auth/popup-blocked') { toast('Popup bloqueado. Permita popups para este site.'); } else { toast('Erro: ' + err.message); } }); } function gcalDisconnect() { localStorage.removeItem('ffos_gcal_token'); localStorage.setItem('ffos_gcal_connected', '0'); _gcalEvents = []; var el = document.getElementById('cal-events'); if (el) el.innerHTML = ''; gcalUpdateUI(); renderGCalPrompt(); toast('Google Calendar desconectado.'); } function gcalFetch() { var token = localStorage.getItem('ffos_gcal_token'); if (!token) return; // Fetch only TODAY's events var start = new Date(curDate); start.setHours(0,0,0,0); var end = new Date(curDate); end.setHours(23,59,59,999); var url = 'https://www.googleapis.com/calendar/v3/calendars/primary/events' + '?timeMin=' + encodeURIComponent(start.toISOString()) + '&timeMax=' + encodeURIComponent(end.toISOString()) + '&singleEvents=true&orderBy=startTime&maxResults=50'; fetch(url, { headers: { Authorization: 'Bearer ' + token } }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.error) { console.error('GCal error:', data.error.code, data.error.message); if (data.error.code === 401 || data.error.code === 403) { localStorage.removeItem('ffos_gcal_token'); localStorage.setItem('ffos_gcal_connected', '0'); gcalUpdateUI(); renderGCalPrompt(); if (data.error.code === 403) toast('Erro 403: Ative a Google Calendar API no Google Cloud Console.'); } return; } _gcalEvents = data.items || []; renderGCalEvents(); }) .catch(function(e) { console.warn('gcalFetch error:', e); }); } function renderGCalEvents() { var promptEl = document.getElementById('cal-prompt'); if (promptEl) promptEl.innerHTML = ''; var el = document.getElementById('cal-events'); if (!el) return; if (!_gcalEvents || !_gcalEvents.length) { el.innerHTML = '
'+t('today_no_events')+'
'; return; } el.innerHTML = _gcalEvents.map(function(ev) { var start = ev.start.dateTime || ev.start.date; var d = new Date(start); var timeStr = ev.start.dateTime ? d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : 'Dia todo'; var loc = ev.location ? '
' + ev.location.substring(0, 60) + '
' : ''; return '
' + '
' + timeStr + '
' + '
' + '
' + (ev.summary || 'Sem título') + '
' + loc + '
' + '
'; }).join(''); } function renderGCalPrompt() { var el = document.getElementById('cal-prompt'); if (!el) return; if (localStorage.getItem('ffos_gcal_connected') === '1') { el.innerHTML = ''; return; } el.innerHTML = '
' + '📅' + 'Conectar Google Calendar para ver eventos' + '' + '
'; } function gcalUpdateUI() { var connected = localStorage.getItem('ffos_gcal_connected') === '1' && !!localStorage.getItem('ffos_gcal_token'); var cb = document.getElementById('gcal-connect-btn'); var db = document.getElementById('gcal-disconnect-btn'); var st = document.getElementById('gcal-status-lbl'); if (cb) cb.style.display = connected ? 'none' : 'inline-flex'; if (db) db.style.display = connected ? 'inline-flex' : 'none'; if (st) st.innerHTML = connected ? ''+t('cfg_gcal_on')+'' : ''+t('cfg_gcal_off')+''; } // ═══════════════════════════════════════════ // MODAL HELPERS // ═══════════════════════════════════════════ function openMo(id) { const el = document.getElementById(id); if(el) el.classList.add('open'); } function closeMo(id) { const el = document.getElementById(id); if(el) el.classList.remove('open'); } // Close modals on overlay click document.querySelectorAll('.modal-overlay').forEach(el => { el.addEventListener('click', function(e) { if(e.target === el) el.classList.remove('open'); }); }); // ═══════════════════════════════════════════ // TOAST // ═══════════════════════════════════════════ let _toastTimer; function toast(msg) { const el = document.getElementById('toast'); if (!el) return; el.textContent = msg; el.classList.add('show'); clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.classList.remove('show'), 2500); } // ═══════════════════════════════════════════ // KEYBOARD SHORTCUTS // ═══════════════════════════════════════════ document.addEventListener('keydown', function(e) { if ((e.metaKey||e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); } if ((e.metaKey||e.ctrlKey) && e.key === 'j') { e.preventDefault(); openCapture(); } if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay.open').forEach(el => el.classList.remove('open')); } }); function showRecentCaptures() { var captures = JSON.parse(localStorage.getItem('ffos_captures') || '[]'); var el = document.getElementById('captures-list'); if (!el) return; if (!captures.length) { el.innerHTML = '
Nenhuma captura por voz ainda.
'; } else { var icons = {task:'✓', diary:'📔', note:'📝'}; el.innerHTML = captures.map(function(c) { return '
' + '
' + (icons[c.type]||'🎤') + '
' + '
' + c.text.substring(0,80) + '
' + '
' + c.type + ' · ' + c.date + ' ' + c.time + '
' + '
'; }).join(''); } openMo('mo-captures'); } function taskRowFull(task) { var cat = CAT_LABELS[task.cat] || ''; var catColor = getCatColor(task.cat); var todayStr = dk(today0()); var overdue = task.date && task.date < todayStr && !task.done; var recurBadge = task.recur ? '↻ '+( RECUR_LABELS[task.recur]||task.recur)+'' : ''; var subtasksHtml = renderSubtasksInline(task); return '
' + '' + '
' + '
' + '
'+escHtml(task.name)+'
' + '
' + (task.date ? ''+task.date+(task.time?' '+task.time:'')+'' : '') + (task.cat ? ''+cat+'' : '') + recurBadge + (task.comments&&task.comments.length ? '💬 '+task.comments.length+'' : '') + '
' + (task.tags&&task.tags.length ? '
'+task.tags.map(function(tg){return ''+escHtml(tg)+'';}).join('')+'
' : '') + subtasksHtml + '
' + '' + '
'; } // ═══════════════════════════════════════════ // GROQ ONBOARDING BANNER // ═══════════════════════════════════════════ function checkGroqOnboarding() { var key = localStorage.getItem('ffos_groq_key'); var dismissed = localStorage.getItem('ffos_groq_dismissed'); var banner = document.getElementById('groq-banner'); if (!key && !dismissed && banner) { setTimeout(function() { banner.style.display = 'block'; }, 3000); } } // ═══════════════════════════════════════════ // TASK COMMENTS // ═══════════════════════════════════════════ var _editComments = []; function addTaskComment() { var inp = document.getElementById('t-comment-input'); var text = inp ? inp.value.trim() : ''; if (!text) return; _editComments.push({ id: uid(), text: text, date: dk(today0()), time: new Date().toLocaleTimeString('pt-BR', {hour:'2-digit', minute:'2-digit'}) }); if (inp) inp.value = ''; renderCommentsModal(); } function renderCommentsModal() { var el = document.getElementById('t-comments-list'); if (!el) return; if (!_editComments.length) { el.innerHTML = ''; return; } el.innerHTML = _editComments.map(function(c) { return '
' + '
' + c.date + ' ' + c.time + '
' + escHtml(c.text) + '
'; }).join(''); } // ═══════════════════════════════════════════ // SWIPE BETWEEN PAGES (mobile) // ═══════════════════════════════════════════ var _swipePageStart = 0; var _swipePageActive = false; var PAGE_ORDER = ['today', 'tasks', 'routines', 'projects', 'library']; function initPageSwipe() { if (window.innerWidth > 768) return; var main = document.getElementById('main'); if (!main || main._pageSwipe) return; main._pageSwipe = true; main.addEventListener('touchstart', function(e) { _swipePageStart = e.touches[0].clientX; _swipePageActive = true; }, {passive: true}); main.addEventListener('touchend', function(e) { if (!_swipePageActive) return; _swipePageActive = false; var dx = e.changedTouches[0].clientX - _swipePageStart; if (Math.abs(dx) < 60) return; // too small var idx = PAGE_ORDER.indexOf(curPage); if (idx < 0) return; if (dx < -60 && idx < PAGE_ORDER.length - 1) { goTo(PAGE_ORDER[idx + 1]); } else if (dx > 60 && idx > 0) { goTo(PAGE_ORDER[idx - 1]); } }, {passive: true}); } // ═══════════════════════════════════════════ // TASK SEARCH (in notes and tags) // ═══════════════════════════════════════════ function getTaskSearchQuery() { var el = document.getElementById('tasks-search'); return el ? el.value.trim().toLowerCase() : ''; } // ═══════════════════════════════════════════ // CAPTURE REPROCESS // ═══════════════════════════════════════════ function reprocessCapture(idx) { var captures = JSON.parse(localStorage.getItem('ffos_captures') || '[]'); var cap = captures[idx]; if (!cap) return; openVoiceDiary(); setTimeout(function() { _vdTranscript = cap.text; var transcript = document.getElementById('vd-transcript'); if (transcript) { transcript.textContent = cap.text; transcript.style.display = 'block'; } var label = document.getElementById('vd-state-label'); if (label) label.textContent = 'Reprocessando...'; processVoiceWithAI(cap.text); }, 300); } // ═══════════════════════════════════════════ // DASHBOARD // ═══════════════════════════════════════════ function renderDashboard() { var tasks = getTasks(); var todayStr = dk(today0()); // Done today var doneToday = tasks.filter(function(t){ return t.done && t.updated === todayStr; }).length; var el = document.getElementById('dash-done-today'); if (el) el.textContent = doneToday; // Open tasks var open = tasks.filter(function(t){ return !t.done; }).length; var el2 = document.getElementById('dash-open'); if (el2) el2.textContent = open; // Weekly chart renderWeeklyChart(tasks); // Streaks renderDashStreaks(); // Voice captures renderDashCaptures(); } function renderWeeklyChart(tasks) { var el = document.getElementById('dash-chart'); if (!el) return; var days = []; var shortDays = TX[lang] ? TX[lang].short_days : ['Dom','Seg','Ter','Qua','Qui','Sex','Sab']; for (var i = 6; i >= 0; i--) { var d = new Date(today0()); d.setDate(d.getDate() - i); days.push({ str: dk(d), label: shortDays[d.getDay()], isToday: i === 0 }); } var max = 1; var counts = days.map(function(day) { var count = tasks.filter(function(t){ return t.done && t.updated === day.str; }).length; if (count > max) max = count; return count; }); el.innerHTML = days.map(function(day, i) { var count = counts[i]; var pct = Math.max(4, Math.round((count / max) * 100)); var barClass = 'chart-bar' + (day.isToday ? ' today' : '') + (count === 0 ? ' empty' : ''); return '
' + '
' + (count || '') + '
' + '
' + '
' + day.label + '
' + '
'; }).join(''); } function renderDashStreaks() { var el = document.getElementById('dash-streaks'); if (!el) return; var routines = getRoutines(); if (!routines.length) { el.innerHTML = '
Nenhuma rotina cadastrada.
'; return; } var withStreak = routines.map(function(r) { return { name: r.name, streak: getStreak(r) }; }).sort(function(a,b){ return b.streak - a.streak; }); el.innerHTML = withStreak.map(function(r) { var fire = r.streak >= 7 ? '🔥' : r.streak >= 3 ? '⚡' : '·'; return '
' + '' + fire + '' + '' + escHtml(r.name) + '' + '' + r.streak + ' dias' + '
'; }).join(''); } function renderDashCaptures() { var el = document.getElementById('dash-captures'); if (!el) return; var captures = JSON.parse(localStorage.getItem('ffos_captures') || '[]'); if (!captures.length) { el.innerHTML = '
Nenhuma captura por voz ainda.
'; return; } var icons = { task: '✓', diary: '📔', note: '📝' }; el.innerHTML = captures.slice(0, 8).map(function(c, idx) { return '
' + '
' + (icons[c.type] || '🎤') + '
' + '
' + '
' + escHtml(c.text.substring(0, 80)) + '
' + '
' + (c.type||'voz') + ' · ' + c.date + ' ' + c.time + '
' + '
↺ Reprocessar com IA
' + '
'; }).join(''); } // ═══════════════════════════════════════════ // SYNC INDICATOR // ═══════════════════════════════════════════ var _syncTimer = null; function showSyncStatus(status) { // status: 'syncing' | 'done' | 'error' var indicator = document.getElementById('sync-indicator'); var dot = document.getElementById('sync-dot'); var label = document.getElementById('sync-label'); if (!indicator) return; indicator.classList.add('show'); clearTimeout(_syncTimer); if (status === 'syncing') { dot.className = 'syncing'; if (label) label.textContent = 'Sincronizando...'; } else if (status === 'done') { dot.className = ''; if (label) label.textContent = 'Sincronizado'; _syncTimer = setTimeout(function() { indicator.classList.remove('show'); }, 2000); } else if (status === 'error') { dot.className = 'error'; if (label) label.textContent = 'Erro ao sincronizar'; _syncTimer = setTimeout(function() { indicator.classList.remove('show'); }, 4000); } } // ═══════════════════════════════════════════ // IMPORT JSON // ═══════════════════════════════════════════ function openImport() { document.getElementById('import-file').click(); } function handleImportDrop(e) { e.preventDefault(); document.getElementById('import-drop').classList.remove('drag'); var file = e.dataTransfer.files[0]; if (file) processImportFile(file); } function handleImportFile(input) { var file = input.files[0]; if (file) processImportFile(file); input.value = ''; } function processImportFile(file) { var reader = new FileReader(); reader.onload = function(e) { try { var data = JSON.parse(e.target.result); var imported = 0; if (data.tasks && Array.isArray(data.tasks)) { setTasks(data.tasks); imported++; } if (data.routines && Array.isArray(data.routines)) { setRoutines(data.routines); imported++; } if (data.projects && Array.isArray(data.projects)) { setProjects(data.projects); imported++; } if (data.library && Array.isArray(data.library)) { setLib(data.library); imported++; } if (imported > 0) { render(); toast('✓ Dados importados com sucesso!'); addNotification('📂', 'Backup importado', imported + ' coleções restauradas'); } else { toast('Arquivo inválido — nenhum dado encontrado.'); } } catch(err) { toast('Erro ao ler o arquivo JSON.'); } }; reader.readAsText(file); } // ═══════════════════════════════════════════ // SUBTASKS // ═══════════════════════════════════════════ var _editSubtasks = []; function addSubtask(text) { _editSubtasks.push({ id: uid(), name: text || '', done: false }); renderSubtasksModal(); // Focus last input setTimeout(function() { var inputs = document.querySelectorAll('#t-subtasks input'); if (inputs.length) inputs[inputs.length-1].focus(); }, 50); } function renderSubtasksModal() { var el = document.getElementById('t-subtasks'); if (!el) return; el.innerHTML = _editSubtasks.map(function(s, i) { return '
' + '
' + '' + '' + '
'; }).join(''); } function handleSubtaskKey(e){if(e.key==="Enter")addSubtask();} function toggleSubtaskEdit(i) { _editSubtasks[i].done = !_editSubtasks[i].done; renderSubtasksModal(); } function updateSubtaskName(i, val) { _editSubtasks[i].name = val; } function removeSubtask(i) { _editSubtasks.splice(i, 1); renderSubtasksModal(); } function renderSubtasksInline(task) { if (!task.subtasks || !task.subtasks.length) return ''; var done = task.subtasks.filter(function(s){return s.done;}).length; var total = task.subtasks.length; var pct = total ? Math.round(done/total*100) : 0; return '
' + task.subtasks.map(function(s) { return '
' + '
' + ''+escHtml(s.name)+'' + '
'; }).join('') + '
' + '
'; } function toggleSubtaskInline(taskId, subId) { var tasks = getTasks(); var task = tasks.find(function(t){return t.id===taskId;}); if (!task || !task.subtasks) return; var sub = task.subtasks.find(function(s){return s.id===subId;}); if (sub) sub.done = !sub.done; task.updated = dk(today0()); setTasks(tasks); render(); } // ═══════════════════════════════════════════ // RECURRENCE // ═══════════════════════════════════════════ var RECUR_LABELS = { daily:'Todo dia', weekly:'Toda semana', monthly:'Todo mês', mon:'Toda segunda', tue:'Toda terça', wed:'Toda quarta', thu:'Toda quinta', fri:'Toda sexta', sat:'Todo sábado', sun:'Todo domingo' }; var RECUR_DAYS = { mon:1, tue:2, wed:3, thu:4, fri:5, sat:6, sun:0 }; function processRecurringTasks() { var tasks = getTasks(); var todayStr = dk(today0()); var todayDay = today0().getDay(); var changed = false; tasks.forEach(function(t) { if (!t.recur || !t.done) return; var shouldRecur = false; if (t.recur === 'daily') shouldRecur = true; else if (t.recur === 'weekly' && t.date) { var orig = new Date(t.date + 'T12:00:00'); shouldRecur = orig.getDay() === todayDay; } else if (t.recur === 'monthly' && t.date) { var origDay = new Date(t.date + 'T12:00:00').getDate(); shouldRecur = today0().getDate() === origDay; } else if (RECUR_DAYS[t.recur] !== undefined) { shouldRecur = RECUR_DAYS[t.recur] === todayDay; } if (shouldRecur && t.date !== todayStr) { // Create new instance for today tasks.push({ id: uid(), name: t.name, date: todayStr, time: t.time || '', cat: t.cat || '', notes: t.notes || '', done: false, starred: false, recur: t.recur, subtasks: t.subtasks ? t.subtasks.map(function(s){ return {id:uid(),name:s.name,done:false}; }) : [], created: Date.now(), updated: todayStr }); t.date = todayStr; // Update last occurrence changed = true; } }); if (changed) setTasks(tasks); } // ═══════════════════════════════════════════ // DRAG TO REORDER // ═══════════════════════════════════════════ var _dragId = null; function initDragAndDrop() { var lists = document.querySelectorAll('#tasks-list, #all-tasks-list'); lists.forEach(function(list) { if (list._dnd) return; list._dnd = true; list.addEventListener('dragstart', function(e) { var row = e.target.closest('[data-tid]'); if (!row) return; _dragId = row.dataset.tid; row.style.opacity = '.4'; e.dataTransfer.effectAllowed = 'move'; }); list.addEventListener('dragover', function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; var row = e.target.closest('[data-tid]'); list.querySelectorAll('[data-tid]').forEach(function(r) { r.classList.toggle('drag-over', r === row && r.dataset.tid !== _dragId); }); }); list.addEventListener('drop', function(e) { e.preventDefault(); var target = e.target.closest('[data-tid]'); if (!target || target.dataset.tid === _dragId) return; var tasks = getTasks(); var fromIdx = tasks.findIndex(function(t){return t.id===_dragId;}); var toIdx = tasks.findIndex(function(t){return t.id===target.dataset.tid;}); if (fromIdx < 0 || toIdx < 0) return; var moved = tasks.splice(fromIdx, 1)[0]; tasks.splice(toIdx, 0, moved); setTasks(tasks); render(); }); list.addEventListener('dragend', function() { list.querySelectorAll('[data-tid]').forEach(function(r) { r.classList.remove('drag-over'); r.style.opacity = ''; }); _dragId = null; }); }); } // ═══════════════════════════════════════════ // DATE FILTERS // ═══════════════════════════════════════════ function getWeekRange() { var start = today0(); var end = new Date(start); end.setDate(end.getDate() + 6); return { start: dk(start), end: dk(end) }; } function getMonthRange() { var now = today0(); var start = new Date(now.getFullYear(), now.getMonth(), 1); var end = new Date(now.getFullYear(), now.getMonth()+1, 0); return { start: dk(start), end: dk(end) }; } // ═══════════════════════════════════════════ // SWIPE TO DELETE // ═══════════════════════════════════════════ function makeSwipeable(el, taskId) { var startX = 0, startY = 0, currentX = 0; var isDragging = false, isHorizontal = null; var THRESHOLD = 80; el.addEventListener('touchstart', function(e) { startX = e.touches[0].clientX; startY = e.touches[0].clientY; currentX = 0; isDragging = true; isHorizontal = null; el.style.transition = 'none'; }, {passive: true}); el.addEventListener('touchmove', function(e) { if (!isDragging) return; var dx = e.touches[0].clientX - startX; var dy = e.touches[0].clientY - startY; // Determine direction on first move if (isHorizontal === null) { isHorizontal = Math.abs(dx) > Math.abs(dy); } if (!isHorizontal) return; e.preventDefault(); currentX = Math.min(0, dx); // only left swipe el.style.transform = 'translateX(' + currentX + 'px)'; var bg = el.parentNode.querySelector('.swipe-delete-bg'); if (bg) { bg.classList.toggle('show', currentX < -30); bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD); } }, {passive: false}); el.addEventListener('touchend', function() { if (!isDragging || !isHorizontal) return; isDragging = false; el.style.transition = 'transform .2s ease'; if (currentX < -THRESHOLD) { // Swipe confirmed — animate out and delete el.style.transform = 'translateX(-110%)'; setTimeout(function() { var tasks = getTasks(); var task = tasks.find(function(t){return t.id === taskId;}); if (task) { setTasks(tasks.filter(function(t){return t.id !== taskId;})); render(); toast('Tarefa removida'); } }, 200); } else { // Snap back el.style.transform = 'translateX(0)'; var bg = el.parentNode.querySelector('.swipe-delete-bg'); if (bg) { bg.classList.remove('show'); bg.style.opacity = '0'; } } currentX = 0; }, {passive: true}); } // Override taskRow to wrap with swipeable container on mobile function taskRowSwipeable(t) { var row = taskRow(t); if (window.innerWidth > 768) return row; // Wrap in swipeable container return '
' + '
🗑 Apagar
' + '
' + row.replace('
', '').replace(/^
/, '') + '
' + '
'; } function initSwipeOnTasks() { if (window.innerWidth > 768) return; document.querySelectorAll('[id^="inner-"]').forEach(function(el) { var taskId = el.id.replace('inner-', ''); if (!el._swipe) { el._swipe = true; makeSwipeable(el, taskId); } }); } // ═══════════════════════════════════════════ // PULL TO REFRESH // ═══════════════════════════════════════════ var _pullStartY = 0; var _pullDist = 0; var _pullActive = false; var PULL_THRESHOLD = 70; function initPullToRefresh() { if (window.innerWidth > 768) return; var el = document.getElementById('page-today'); if (!el || el._pull) return; el._pull = true; el.addEventListener('touchstart', function(e) { // Only activate if scrolled to top var scroller = el.querySelector('.today-cols') || el.querySelector('.today-main'); if (scroller && scroller.scrollTop > 5) return; _pullStartY = e.touches[0].clientY; _pullDist = 0; _pullActive = true; }, {passive: true}); el.addEventListener('touchmove', function(e) { if (!_pullActive) return; var dy = e.touches[0].clientY - _pullStartY; if (dy < 0) { _pullActive = false; return; } _pullDist = Math.min(dy, PULL_THRESHOLD * 1.5); var indicator = document.getElementById('pull-indicator'); var text = document.getElementById('pull-text'); if (indicator) { var h = Math.min(52, _pullDist * 0.6); indicator.style.height = h + 'px'; if (text) text.textContent = _pullDist > PULL_THRESHOLD ? 'Soltar para atualizar' : 'Puxar para atualizar'; } }, {passive: true}); el.addEventListener('touchend', function() { if (!_pullActive) return; _pullActive = false; var indicator = document.getElementById('pull-indicator'); if (_pullDist > PULL_THRESHOLD) { // Trigger refresh if (indicator) { indicator.classList.add('show', 'loading'); } var text = document.getElementById('pull-text'); if (text) text.textContent = 'Atualizando...'; // Refresh calendar and data if (localStorage.getItem('ffos_gcal_connected') === '1') { gcalFetch(); } pullCloud(window._fbUid); setTimeout(function() { if (indicator) { indicator.classList.remove('show', 'loading'); indicator.style.height = '0'; } toast('Atualizado!'); }, 1500); } else { if (indicator) { indicator.style.height = '0'; } } _pullDist = 0; }, {passive: true}); } // ═══════════════════════════════════════════ // OFFLINE MODE // ═══════════════════════════════════════════ function initOfflineDetection() { function onOnline() { toast('🌐 Conexão restaurada'); // Re-sync when back online if (window._fbUid) pullCloud(window._fbUid); if (localStorage.getItem('ffos_gcal_connected') === '1') gcalFetch(); } function onOffline() { toast('📵 Sem conexão — modo offline'); } window.addEventListener('online', onOnline); window.addEventListener('offline', onOffline); } // ═══════════════════════════════════════════ // PUSH NOTIFICATIONS (Web Push API) // ═══════════════════════════════════════════ function initWebPush() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) return; // Already handled by requestPushPermission } // ═══════════════════════════════════════════ // CAPTURE SHEET // ═══════════════════════════════════════════ function openCaptureSheet() { document.getElementById('capture-sheet').classList.add('open'); document.getElementById('sheet-overlay').classList.add('show'); } function closeCaptureSheet() { document.getElementById('capture-sheet').classList.remove('open'); document.getElementById('sheet-overlay').classList.remove('show'); } // ═══════════════════════════════════════════ // GEOLOCATION // ═══════════════════════════════════════════ function getCity() { var cityEl = document.getElementById('mob-city'); if (!cityEl) return; if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition(function(pos) { var lat = pos.coords.latitude; var lon = pos.coords.longitude; fetch('https://nominatim.openstreetmap.org/reverse?lat='+lat+'&lon='+lon+'&format=json') .then(function(r){ return r.json(); }) .then(function(d) { var city = d.address && (d.address.city || d.address.town || d.address.municipality || d.address.state); if (city && cityEl) cityEl.textContent = city; }) .catch(function(){}); }, function(){ // Permission denied - keep Rio de Janeiro default }); } // Update mobile date header function updateMobDateHeader() { var txData = TX[lang] || TX['pt']; var days = txData.days; var months = txData.months; var mobBig = document.getElementById('mob-date-big'); var mobBtn = document.getElementById('mob-today-btn'); var todayStr = dk(today0()); var isToday = dk(curDate) === todayStr; // Format: "Segunda-feira, Junho 15" if (mobBig) mobBig.textContent = days[curDate.getDay()] + ', ' + months[curDate.getMonth()] + ' ' + curDate.getDate(); if (mobBtn) { mobBtn.textContent = lang === 'en' ? 'Today' : 'Hoje'; mobBtn.className = 'mob-today-btn' + (isToday ? ' current' : ''); } } // ═══════════════════════════════════════════ // NOTIFICATIONS // ═══════════════════════════════════════════ var _notifications = []; function addNotification(icon, title, body) { _notifications.unshift({ id: uid(), icon: icon, title: title, body: body, time: new Date().toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'}), date: dk(today0()), read: false }); if (_notifications.length > 20) _notifications = _notifications.slice(0,20); localStorage.setItem('ffos_notifications', JSON.stringify(_notifications)); updateBellBadge(); } function updateBellBadge() { var badge = document.getElementById('bell-badge'); var unread = _notifications.filter(function(n){return !n.read;}).length; if (badge) badge.style.display = unread > 0 ? 'block' : 'none'; } function openNotifications() { // Mark all as read _notifications.forEach(function(n){n.read=true;}); localStorage.setItem('ffos_notifications', JSON.stringify(_notifications)); updateBellBadge(); var el = document.getElementById('notif-list'); if (!el) return; if (!_notifications.length) { el.innerHTML = '
Nenhuma notificação.
'; } else { el.innerHTML = _notifications.map(function(n) { return '
' + '
'+n.icon+'
' + '
' + '
'+n.title+'
' + '
'+n.body+'
' + '
'+n.date+' '+n.time+'
' + '
'; }).join(''); } openMo('mo-notif'); } function clearNotifications() { _notifications = []; localStorage.removeItem('ffos_notifications'); updateBellBadge(); closeMo('mo-notif'); toast('Notificações limpas.'); } // Init notifications from storage function initNotifications() { _notifications = JSON.parse(localStorage.getItem('ffos_notifications') || '[]'); updateBellBadge(); } // Override showPushNotification to also add to notification center var _origShowPush = null; function initNotifOverride() { _origShowPush = showPushNotification; showPushNotification = function(title, body) { addNotification('🔔', title, body); if (Notification.permission === 'granted') { try { new Notification(title, {body:body, icon:'/icon-192.png'}); } catch(e) { showPushBanner(title + ' — ' + body); } } else { showPushBanner(title + ' — ' + body); } }; } // ═══════════════════════════════════════════ // CREATE GOOGLE CALENDAR EVENT // ═══════════════════════════════════════════ function openCreateEvent() { var el = document.getElementById('ev-date'); if (el) el.value = dk(today0()); var s = document.getElementById('ev-start'); if (s) s.value = new Date().toTimeString().substring(0,5); var e2 = document.getElementById('ev-end'); if (e2) { var d = new Date(); d.setHours(d.getHours()+1); e2.value = d.toTimeString().substring(0,5); } openMo('mo-create-event'); } function createCalendarEvent() { var token = localStorage.getItem('ffos_gcal_token'); if (!token) { toast('Conecte o Google Calendar primeiro.'); return; } var title = document.getElementById('ev-title').value.trim(); if (!title) { toast('Digite um título para o evento.'); return; } var date = document.getElementById('ev-date').value; var start = document.getElementById('ev-start').value; var end = document.getElementById('ev-end').value; var desc = document.getElementById('ev-desc').value; if (!date || !start || !end) { toast('Preencha data e horários.'); return; } var body = { summary: title, description: desc, start: { dateTime: date+'T'+start+':00', timeZone: 'America/Sao_Paulo' }, end: { dateTime: date+'T'+end+':00', timeZone: 'America/Sao_Paulo' } }; fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', { method: 'POST', headers: { 'Authorization': 'Bearer '+token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .then(function(r){ return r.json(); }) .then(function(d){ if (d.error) { toast('Erro: '+d.error.message); } else { closeMo('mo-create-event'); document.getElementById('ev-title').value = ''; document.getElementById('ev-desc').value = ''; toast('✓ Evento criado no Calendar!'); gcalFetch(); } }) .catch(function(){ toast('Erro ao criar evento.'); }); } // ═══════════════════════════════════════════ // CALENDAR EVENT COLORS BY TYPE // ═══════════════════════════════════════════ function getEventColor(ev) { var title = (ev.summary || '').toLowerCase(); if (/aula|aluno|turma|inglês|espanhol|lesson|class/.test(title)) return '#7c3aed'; if (/reunião|meeting|call|zoom|teams/.test(title)) return '#1d4ed8'; if (/almoço|jantar|café|lunch|dinner/.test(title)) return '#0f9d58'; if (/médico|saúde|academia|health|doctor/.test(title)) return '#db4437'; if (/pagamento|cobrança|financeiro|boleto/.test(title)) return '#d97706'; return '#4285f4'; } // ═══════════════════════════════════════════ // NOTIFICATION SETTINGS // ═══════════════════════════════════════════ var _notifSettings = {}; function loadNotifSettings() { _notifSettings = JSON.parse(localStorage.getItem('ffos_notif_settings') || '{}'); var defaults = { morning:'07:00', afternoon:'13:00', evening:'20:00', morning_on:true, afternoon_on:true, evening_on:true, calendar_on:true }; Object.keys(defaults).forEach(function(k){ if(_notifSettings[k]===undefined) _notifSettings[k]=defaults[k]; }); } function saveNotifTimes() { ['morning','afternoon','evening'].forEach(function(p){ var el = document.getElementById('notif-'+p); if (el) _notifSettings[p] = el.value; }); localStorage.setItem('ffos_notif_settings', JSON.stringify(_notifSettings)); toast('Horários salvos!'); } function toggleNotif(key) { loadNotifSettings(); _notifSettings[key+'_on'] = !_notifSettings[key+'_on']; localStorage.setItem('ffos_notif_settings', JSON.stringify(_notifSettings)); updateNotifToggles(); } function updateNotifToggles() { loadNotifSettings(); ['morning','afternoon','evening','calendar'].forEach(function(k){ var el = document.getElementById('toggle-'+k); if (el) el.classList.toggle('on', !!_notifSettings[k+'_on']); var inp = document.getElementById('notif-'+k); if (inp && _notifSettings[k]) inp.value = _notifSettings[k]; }); } // Check for calendar event reminders (15 min before) function checkCalendarReminders() { if (!_notifSettings.calendar_on) return; if (!_gcalEvents || !_gcalEvents.length) return; var now = new Date(); var soon = new Date(now.getTime() + 15*60*1000); _gcalEvents.forEach(function(ev){ if (!ev.start || !ev.start.dateTime) return; var evTime = new Date(ev.start.dateTime); if (evTime > now && evTime <= soon) { var key = 'reminded_'+ev.id; if (!sessionStorage.getItem(key)) { sessionStorage.setItem(key, '1'); showPushNotification('📅 Em 15 min: '+ev.summary, new Date(ev.start.dateTime).toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'})); } } }); } // ═══════════════════════════════════════════ // COMPACT MODE // ═══════════════════════════════════════════ function toggleCompact() { var isCompact = document.body.classList.toggle('compact'); localStorage.setItem('ffos_compact', isCompact ? '1' : '0'); var el = document.getElementById('toggle-compact'); if (el) el.classList.toggle('on', isCompact); } function initCompactMode() { var isCompact = localStorage.getItem('ffos_compact') === '1'; if (isCompact) document.body.classList.add('compact'); var el = document.getElementById('toggle-compact'); if (el) el.classList.toggle('on', isCompact); } // ═══════════════════════════════════════════ // CUSTOM ACCENT COLOR // ═══════════════════════════════════════════ function setAccentColor(color) { document.documentElement.style.setProperty('--accent', color); // Update sidebar-bg contrast var isDark = isColorDark(color); document.documentElement.style.setProperty('--sidebar-bg-contrast', isDark ? '#fff' : '#000'); localStorage.setItem('ffos_accent', color); updateAccentSwatches(color); } function isColorDark(hex) { var r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16); return (r*299+g*587+b*114)/1000 < 128; } function updateAccentSwatches(color) { document.querySelectorAll('.accent-swatch').forEach(function(s){ s.style.border = '2px solid '+(s.getAttribute('onclick').includes(color) ? color : 'transparent'); }); var ci = document.getElementById('custom-accent'); if (ci) ci.value = color; } function initAccentColor() { var saved = localStorage.getItem('ffos_accent'); if (saved) { setAccentColor(saved); } } // ═══════════════════════════════════════════ // AI SUGGESTIONS // ═══════════════════════════════════════════ function suggestTop3WithAI() { var groqKey = localStorage.getItem('ffos_groq_key'); if (!groqKey) return; var tasks = getTasks().filter(function(t){ return !t.done; }).slice(0, 20); var calToday = (_gcalEvents||[]).map(function(ev){ return (ev.start&&ev.start.dateTime ? new Date(ev.start.dateTime).toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'}) : '') + ' ' + ev.summary; }).join('; '); var prompt = 'Você é um assistente de produtividade. Com base nas tarefas abertas e agenda de hoje, sugira as 3 tarefas mais importantes para o dia. ' + 'Tarefas abertas: ' + tasks.map(function(t){return t.name+(t.date?' ('+t.date+')':'');}).join(', ') + '. ' + 'Agenda de hoje: ' + (calToday||'nenhum evento') + '. ' + 'Responda SOMENTE em JSON: {"top3":["tarefa1","tarefa2","tarefa3"],"motivo":"explicação breve"}'; fetch('https://api.groq.com/openai/v1/chat/completions', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+groqKey}, body:JSON.stringify({model:'llama-3.3-70b-versatile',max_tokens:200,temperature:0.3, messages:[{role:'user',content:prompt}]}) }).then(function(r){return r.json();}) .then(function(d){ var text = d.choices&&d.choices[0]?d.choices[0].message.content:''; try { var clean = text.replace(/```json|```/g,'').trim(); var parsed = JSON.parse(clean); showTop3Suggestions(parsed); } catch(e){} }).catch(function(){}); } function showTop3Suggestions(data) { var el = document.getElementById('ai-top3-suggestions'); if (!el || !data.top3) return; el.innerHTML = '
💡 IA sugere para hoje:
' + data.top3.map(function(s,i){ return '
' + ''+(i===0?'1️⃣':i===1?'2️⃣':'3️⃣')+'' + '
'+escHtml(s)+'
'; }).join('') + (data.motivo?'
'+escHtml(data.motivo)+'
':''); el.style.display = 'block'; } function useAISuggestion(name) { var tasks = getTasks(); var match = tasks.find(function(t){ return t.name.toLowerCase().includes(name.toLowerCase()) && !t.done; }); if (match) { match.starred = true; setTasks(tasks); render(); toast('⭐ Adicionada ao Top 3!'); } else { openAddTask(); setTimeout(function(){ var el = document.getElementById('t-name'); if (el) el.value = name; }, 100); } var sugEl = document.getElementById('ai-top3-suggestions'); if (sugEl) sugEl.style.display = 'none'; } // End of day summary function requestDaySummary() { var groqKey = localStorage.getItem('ffos_groq_key'); if (!groqKey) { toast('Configure a Groq API Key para usar a IA.'); return; } var tasks = getTasks(); var todayStr = dk(today0()); var done = tasks.filter(function(t){ return t.done && t.updated===todayStr; }); var pending = tasks.filter(function(t){ return !t.done && t.date===todayStr; }); var overdue = tasks.filter(function(t){ return !t.done && t.date && t.date < todayStr; }); var prompt = 'Faça um resumo do dia de forma encorajadora e concisa (máximo 3 linhas). ' + 'Concluídas hoje: ' + done.map(function(t){return t.name;}).join(', ') + '. ' + 'Pendentes do dia: ' + pending.map(function(t){return t.name;}).join(', ') + '. ' + 'Atrasadas: ' + overdue.map(function(t){return t.name;}).join(', ') + '. ' + 'Responda em português.'; fetch('https://api.groq.com/openai/v1/chat/completions', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+groqKey}, body:JSON.stringify({model:'llama-3.3-70b-versatile',max_tokens:150, messages:[{role:'user',content:prompt}]}) }).then(function(r){return r.json();}) .then(function(d){ var text = d.choices&&d.choices[0]?d.choices[0].message.content:''; if (text) showDaySummary(text); }).catch(function(){}); } function showDaySummary(text) { var el = document.getElementById('day-summary-content'); if (el) { el.textContent = text; document.getElementById('day-summary-box').style.display = 'block'; } else { toast(text.substring(0,80)+'...'); } } // Suggest recurrence for repeating tasks function detectRecurringPatterns() { var tasks = getTasks(); var nameCount = {}; tasks.filter(function(t){ return t.done; }).forEach(function(t){ var key = t.name.toLowerCase().trim(); if (!nameCount[key]) nameCount[key] = []; nameCount[key].push(t.date); }); var suggestions = []; Object.keys(nameCount).forEach(function(name){ if (nameCount[name].length >= 3 && !tasks.find(function(t){ return t.name.toLowerCase()===name && t.recur; })) { suggestions.push(name); } }); if (suggestions.length > 0) { var existing = getTasks().filter(function(t){ return !t.done && !t.recur; }); var toSuggest = existing.filter(function(t){ return suggestions.includes(t.name.toLowerCase().trim()); }); toSuggest.slice(0,3).forEach(function(t){ addNotification('↻', 'Tarefa repetida detectada', '"'+t.name+'" — deseja adicionar recorrência?'); }); } } // ═══════════════════════════════════════════ // GLOBAL SEARCH (⌘K) // ═══════════════════════════════════════════ var _gsSelected = -1; var _gsResults = []; function openGlobalSearch() { var overlay = document.getElementById('global-search-overlay'); if (overlay) overlay.classList.add('open'); setTimeout(function() { var inp = document.getElementById('global-search-input'); if (inp) { inp.value = ''; inp.focus(); } }, 50); runGlobalSearch(''); } function closeGlobalSearch() { var overlay = document.getElementById('global-search-overlay'); if (overlay) overlay.classList.remove('open'); } function runGlobalSearch(q) { q = (q||'').toLowerCase().trim(); var el = document.getElementById('global-search-results'); if (!el) return; _gsResults = []; _gsSelected = -1; var tasks = getTasks().filter(function(t) { return !q || (t.name||'').toLowerCase().includes(q) || (t.notes||'').toLowerCase().includes(q) || (t.tags||[]).some(function(tg){return tg.toLowerCase().includes(q);}); }); var projects = getProjects().filter(function(p) { return !q || (p.name||'').toLowerCase().includes(q) || (p.desc||'').toLowerCase().includes(q); }); var library = getLib().filter(function(l) { return !q || (l.title||'').toLowerCase().includes(q) || (l.body||'').toLowerCase().includes(q); }); if (!tasks.length && !projects.length && !library.length) { el.innerHTML = '
'+(q?'Nenhum resultado para "'+escHtml(q)+'"':'Digite para pesquisar em tudo')+'
'; return; } var html2 = ''; if (tasks.length) { html2 += '
'; tasks.slice(0,6).forEach(function(t) { var idx = _gsResults.length; _gsResults.push({type:'task', id:t.id}); var catColor = getCatColor(t.cat); html2 += '
' + '
' + '
' + '
'+escHtml(t.name)+'
' + '
'+(t.date||'Sem data')+(t.cat?' · '+t.cat:'')+'
' + '
' + (t.done?'Concluída':t.starred?'':'') + '
'; }); html2 += '
'; } if (projects.length) { html2 += '
'; projects.slice(0,4).forEach(function(p) { var idx = _gsResults.length; _gsResults.push({type:'project', id:p.id}); html2 += '
' + '
' + '
' + '
'+escHtml(p.name)+'
' + '
'+escHtml((p.desc||'').substring(0,50))+'
' + '
' + ''+p.status+'' + '
'; }); html2 += '
'; } if (library.length) { html2 += '
'; library.slice(0,4).forEach(function(l) { var idx = _gsResults.length; _gsResults.push({type:'lib', id:l.id}); html2 += '
' + '
📝
' + '
' + '
'+escHtml(l.title||'Sem título')+'
' + '
'+escHtml((l.body||'').substring(0,50))+'
' + '
' + '
'; }); html2 += '
'; } el.innerHTML = html2; } function gsOpen(idx) { var item = _gsResults[idx]; if (!item) return; closeGlobalSearch(); if (item.type === 'task') { goTo('tasks'); setTimeout(function(){ openEditTask(item.id); }, 100); } else if (item.type === 'project') { goTo('projects'); setTimeout(function(){ openEditProject(item.id); }, 100); } else if (item.type === 'lib') { goTo('library'); setTimeout(function(){ openEditLib(item.id); }, 100); } } function handleSearchKey(e) { var items = document.querySelectorAll('#global-search-results .gs-item'); if (e.key === 'ArrowDown') { _gsSelected = Math.min(_gsSelected+1, items.length-1); } else if (e.key === 'ArrowUp') { _gsSelected = Math.max(_gsSelected-1, 0); } else if (e.key === 'Enter') { if (_gsSelected >= 0 && _gsResults[_gsSelected]) gsOpen(_gsSelected); return; } else if (e.key === 'Escape') { closeGlobalSearch(); return; } items.forEach(function(el,i){ el.classList.toggle('selected', i===_gsSelected); }); if (items[_gsSelected]) items[_gsSelected].scrollIntoView({block:'nearest'}); } // ⌘K / Ctrl+K shortcut document.addEventListener('keydown', function(e) { if ((e.metaKey||e.ctrlKey) && e.key==='k') { e.preventDefault(); var overlay = document.getElementById('global-search-overlay'); if (overlay && overlay.classList.contains('open')) closeGlobalSearch(); else openGlobalSearch(); } if (e.key==='Escape') closeGlobalSearch(); }); // ═══════════════════════════════════════════ // KANBAN VIEW // ═══════════════════════════════════════════ var _kanbanMode = false; function toggleKanban() { _kanbanMode = !_kanbanMode; var btn = document.getElementById('btn-kanban'); var list = document.getElementById('tasks-list'); var kanban = document.getElementById('kanban-view'); var sortBar = document.querySelector('.sort-bar'); if (btn) btn.classList.toggle('active', _kanbanMode); if (list) list.style.display = _kanbanMode ? 'none' : ''; if (kanban) kanban.style.display = _kanbanMode ? 'block' : 'none'; if (sortBar) sortBar.style.display = _kanbanMode ? 'none' : ''; if (_kanbanMode) renderKanban(); } function renderKanban() { var tasks = getTasks(); var todo = tasks.filter(function(t){ return !t.done && t.status !== 'progress'; }); var progress = tasks.filter(function(t){ return t.status === 'progress'; }); var done = tasks.filter(function(t){ return t.done; }).slice(0,20); function cardHtml(t) { var catColor = getCatColor(t.cat); var card = '
' + '
'+escHtml(t.name)+'
' + '
' + (t.date?''+t.date+'':'') + (t.cat?''+t.cat+'':'') + (t.starred?'':'') + '
'; if (!t.done && t.status!=='progress') { card += ''; } if (t.status==='progress') { card += ''; } if (!t.done) { card += ''; } card += '
'; return card; } var kTodo = document.getElementById('k-todo'); var kProg = document.getElementById('k-progress'); var kDone = document.getElementById('k-done'); if (kTodo) { kTodo.innerHTML = todo.map(cardHtml).join(''); document.getElementById('k-todo-count').textContent = todo.length; } if (kProg) { kProg.innerHTML = progress.map(cardHtml).join(''); document.getElementById('k-progress-count').textContent = progress.length; } if (kDone) { kDone.innerHTML = done.map(cardHtml).join(''); document.getElementById('k-done-count').textContent = done.length; } } function moveKanban(taskId, dest) { var tasks = getTasks(); var t = tasks.find(function(x){return x.id===taskId;}); if (!t) return; if (dest==='done') { t.done=true; t.updated=dk(today0()); delete t.status; } else if (dest==='progress') { t.done=false; t.status='progress'; } else { t.done=false; delete t.status; } setTasks(tasks); renderKanban(); render(); } // ═══════════════════════════════════════════ // SORT // ═══════════════════════════════════════════ var _taskSort = 'date'; function setSort(s, btn) { _taskSort = s; document.querySelectorAll('.sort-btn').forEach(function(b){ b.classList.remove('active'); }); if (btn) btn.classList.add('active'); renderTasks(); } function applySort(tasks) { var sorted = tasks.slice(); if (_taskSort === 'date') { sorted.sort(function(a,b){ if(a.date&&b.date) return a.date>b.date?1:-1; if(a.date) return -1; if(b.date) return 1; return a.name>b.name?1:-1; }); } else if (_taskSort === 'priority') { sorted.sort(function(a,b){ return (b.starred?1:0)-(a.starred?1:0); }); } else if (_taskSort === 'cat') { sorted.sort(function(a,b){ return (a.cat||'zzz')>(b.cat||'zzz')?1:-1; }); } else if (_taskSort === 'name') { sorted.sort(function(a,b){ return a.name>b.name?1:-1; }); } return sorted; } // ═══════════════════════════════════════════ // DASHBOARD // ═══════════════════════════════════════════ function renderDashboard() { var tasks = getTasks(); var todayStr = dk(today0()); // Done today var doneToday = tasks.filter(function(t){ return t.done && t.updated===todayStr; }).length; var el = document.getElementById('dash-done-today'); if (el) el.textContent = doneToday; // Open var open = tasks.filter(function(t){ return !t.done; }).length; var el2 = document.getElementById('dash-open'); if (el2) el2.textContent = open; // Weekly goal var goal = parseInt(localStorage.getItem('ffos_dash_goal')||'20'); var inp = document.getElementById('dash-goal-input'); if (inp) inp.value = goal; var week = getWeekRange ? getWeekRange() : {start:todayStr,end:todayStr}; var doneWeek = tasks.filter(function(t){ return t.done && t.updated>=week.start && t.updated<=week.end; }).length; var pct = Math.min(100, Math.round(doneWeek/goal*100)); var bar = document.getElementById('dash-goal-bar'); var lbl = document.getElementById('dash-goal-label'); var pctEl = document.getElementById('dash-goal-pct'); if (bar) bar.style.width = pct+'%'; if (lbl) lbl.textContent = doneWeek+' de '+goal; if (pctEl) pctEl.textContent = pct+'%'; // Weekly chart renderWeeklyChart(tasks); // By category renderCatChart(tasks); // Streaks renderDashStreaks(); } function saveDashGoal(val) { localStorage.setItem('ffos_dash_goal', val); renderDashboard(); } function renderWeeklyChart(tasks) { var el = document.getElementById('dash-chart'); if (!el) return; var shortDays = ['Dom','Seg','Ter','Qua','Qui','Sex','Sab']; var days = []; for (var i=6;i>=0;i--) { var d=new Date(today0()); d.setDate(d.getDate()-i); days.push({str:dk(d),label:shortDays[d.getDay()],isToday:i===0}); } var max=1; var counts=days.map(function(day){ var c=tasks.filter(function(t){return t.done&&t.updated===day.str;}).length; if(c>max)max=c; return c; }); el.innerHTML=days.map(function(day,i){ var c=counts[i]; var pct=Math.max(4,Math.round(c/max*100)); var color=day.isToday?'var(--blue)':'var(--accent)'; return '
' +'
'+(c||'')+'
' +'
' +'
'+day.label+'
' +'
'; }).join(''); } function renderCatChart(tasks) { var el = document.getElementById('dash-by-cat'); if (!el) return; var cats = ['aula','admin','cobranca','pessoal']; var catNames = {aula:'Aulas',admin:'Admin',cobranca:'Cobranças',pessoal:'Pessoal'}; var total = tasks.filter(function(t){return t.done;}).length || 1; var rows = cats.map(function(cat){ var count = tasks.filter(function(t){return t.done && t.cat===cat;}).length; var pct = Math.round(count/total*100); var color = getCatColor(cat); return {name:catNames[cat],count:count,pct:pct,color:color}; }).filter(function(r){return r.count>0;}); if (!rows.length) { el.innerHTML='
Nenhuma tarefa concluída ainda.
'; return; } el.innerHTML = rows.map(function(r){ return '
' +'
'+r.name+'
' +'
' +'
'+r.count+'
' +'
'; }).join(''); } function renderDashStreaks() { var el = document.getElementById('dash-streaks'); if (!el) return; var routines = getRoutines(); if (!routines.length) { el.innerHTML='
Nenhuma rotina cadastrada.
'; return; } el.innerHTML = routines.map(function(r){ var streak = getStreak ? getStreak(r) : 0; var fire = streak>=7?'🔥':streak>=3?'⚡':'·'; return '
' +''+fire+'' +''+escHtml(r.name)+'' +''+streak+' dias' +'
'; }).join(''); } // ═══════════════════════════════════════════ // PDF REPORT // ═══════════════════════════════════════════ function exportPDFReport() { var tasks = getTasks(); var todayStr = dk(today0()); var week = getWeekRange ? getWeekRange() : {start:todayStr,end:todayStr}; var doneWeek = tasks.filter(function(t){ return t.done && t.updated>=week.start && t.updated<=week.end; }); var openTasks = tasks.filter(function(t){ return !t.done; }); var content = '' +'Fast Fluent OS v2.0617.1631' +'' +'

Fast Fluent OS — Relatório Semanal

' +'

'+week.start+' a '+week.end+'

' +'

✓ Concluídas esta semana ('+doneWeek.length+')

' + doneWeek.map(function(t){ return '
'+escHtml(t.name)+''+t.updated+'
'; }).join('') +'

⏳ Em aberto ('+openTasks.length+')

' + openTasks.slice(0,20).map(function(t){ return '
'+escHtml(t.name)+''+(t.date||'Sem data')+'
'; }).join('') +''; var w = window.open('','_blank'); if (w) { w.document.write(content); w.document.close(); setTimeout(function(){ w.print(); }, 500); } else toast('Permita popups para exportar o PDF.'); } // ═══════════════════════════════════════════ // CLOUD SYNC (Firestore) // ═══════════════════════════════════════════ function pushCloud(key, value) { if (!window._db || !window._fbUid) return; showSyncStatus('syncing'); window._db.collection('users').doc(window._fbUid).set({ [key]: value }, { merge: true }) .then(function() { showSyncStatus('done'); }) .catch(function(e) { console.warn('Firestore write error:', e); showSyncStatus('error'); }); } function pullCloud(uid) { if (!window._db) return; window._db.collection('users').doc(uid).get().then(function(snap) { if (snap.exists) { var data = snap.data(); ['tasks','routines','projects','library','profile'].forEach(function(k) { if (data[k] !== undefined) localStorage.setItem('ffos_'+k, JSON.stringify(data[k])); }); } render(); // Real-time listener window._db.collection('users').doc(uid).onSnapshot(function(s) { if (!s.exists) return; var d = s.data(); ['tasks','routines','projects','library'].forEach(function(k) { if (d[k] !== undefined) localStorage.setItem('ffos_'+k, JSON.stringify(d[k])); }); render(); }); }).catch(function() { render(); }); } // ═══════════════════════════════════════════ // LOGIN / LOGOUT // ═══════════════════════════════════════════ function doGoogleLogin() { var btn = document.getElementById('login-btn'); var err = document.getElementById('login-err'); if (err) err.textContent = ''; if (!window._auth) { if(err) err.textContent = 'Carregando... tente em 3s.'; return; } if (btn) { btn.disabled = true; btn.textContent = 'Entrando...'; } var keep = document.getElementById('login-keep'); var persistence = (keep && keep.checked) ? firebase.auth.Auth.Persistence.LOCAL : firebase.auth.Auth.Persistence.SESSION; window._auth.setPersistence(persistence).then(function() { var p = new firebase.auth.GoogleAuthProvider(); p.addScope('https://www.googleapis.com/auth/calendar.readonly'); return window._auth.signInWithPopup(p); }).then(function(result) { if (btn) { btn.disabled = false; btn.innerHTML = 'Continuar com Google'; } if (result.credential && result.credential.accessToken) { localStorage.setItem('ffos_gcal_token', result.credential.accessToken); localStorage.setItem('ffos_gcal_connected', '1'); } }).catch(function(e) { if (btn) { btn.disabled = false; btn.innerHTML = 'Continuar com Google'; } var msgs = { 'auth/popup-blocked': 'Permita popups para este site.', 'auth/popup-closed-by-user': 'Login cancelado.', 'auth/unauthorized-domain': 'Domínio não autorizado no Firebase.', 'auth/operation-not-allowed': 'Ative o login com Google no Firebase Console.' }; if (err) err.textContent = msgs[e.code] || e.message; }); } function doLogout() { if (window._auth) window._auth.signOut(); localStorage.removeItem('ffos_gcal_token'); localStorage.setItem('ffos_gcal_connected', '0'); } // ═══════════════════════════════════════════ // INIT // ═══════════════════════════════════════════ document.addEventListener('DOMContentLoaded', function() { // Force clear old SW caches if version mismatch if ('caches' in window) { caches.keys().then(function(keys) { keys.filter(function(k){ return k !== 'ffos-v8'; }) .forEach(function(k){ caches.delete(k); }); }); } lang = localStorage.getItem('ffos_lang') || 'pt'; applyTranslations(); initNotifications(); // Set date immediately on load curDate = today0(); updateMobDateHeader(); updateDateHeader(); // Init offline detection initOfflineDetection(); // Update date display immediately (render() called after Firebase ready) setTimeout(function() { updateMobDateHeader(); updateDateHeader(); }, 50); // Theme var theme = localStorage.getItem('ffos_theme') || 'light'; document.documentElement.setAttribute('data-theme', theme === 'dark' ? 'dark' : ''); var themeBtn = document.getElementById('theme-btn'); if (themeBtn) themeBtn.textContent = theme === 'dark' ? '☀' : '◐'; // Cat colors applyCatColors(); // Lang lang = localStorage.getItem('ffos_lang') || 'pt'; }); function checkForUpdates() { var el = document.getElementById('app-version-label'); if (el) el.textContent = 'v' + APP_VERSION + ' · Verificando...'; if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage('skipWaiting'); } setTimeout(function() { if (el) el.textContent = 'v' + APP_VERSION + ' · ' + APP_BUILD + ' ✓'; toast('Versão ' + APP_VERSION + ' — atualizado!'); }, 1500); } let _neId=null,_neLinkTimer=null,_neBody=null,_graphOpen=false,_graphSim=null,_activityOpen=false,_fabOpen=false; function openNoteEditor(id,type){_neId=id||null;_neBody=document.getElementById('note-editor-content');const ov=document.getElementById('note-editor-overlay');const titleEl=document.getElementById('editor-title');const metaEl=document.getElementById('editor-meta');const badge=document.getElementById('editor-date-badge');if(!ov||!_neBody)return;const today=dk(today0());if(badge)badge.textContent='📅 '+today;if(id){const item=getLib().find(l=>l.id===id);if(!item)return;if(titleEl)titleEl.value=item.title||'';_neBody.innerHTML=neMarkdownToHtml(item.body||'');if(metaEl)metaEl.textContent=item.date||today;neRenderTags(item.tags?item.tags.split(',').filter(Boolean):[]);}else{if(titleEl)titleEl.value='';_neBody.innerHTML='';if(metaEl)metaEl.textContent=today;neRenderTags([]);}ov.setAttribute('data-type',type||'nota');ov.classList.add('open');setTimeout(()=>{if(!id&&titleEl)titleEl.focus();else _neBody.focus();},100);} function neRenderTags(tags){const row=document.getElementById('editor-tags-row');if(!row)return;row.innerHTML=tags.map(t=>''+escHtml(t.trim())+'').join('')+'+ tag';} function neAddTag(){const tag=prompt('Nova tag:');if(!tag)return;const row=document.getElementById('editor-tags-row');const tags=Array.from(row.querySelectorAll('.etag')).map(c=>c.dataset.tag);tags.push(tag.trim());neRenderTags(tags);} function neGetTags(){const row=document.getElementById('editor-tags-row');return Array.from(row?row.querySelectorAll('.etag'):[]).map(c=>c.dataset.tag).join(',');} function closeNoteEditor(){const ov=document.getElementById('note-editor-overlay');if(ov)ov.classList.remove('open');neHideAC();_neId=null;} function saveNoteEditor(){let title=document.getElementById('editor-title').value.trim();const body=neHtmlToMarkdown(_neBody?_neBody.innerHTML:'');const type=document.getElementById('note-editor-overlay').getAttribute('data-type')||'nota';const tags=neGetTags();const today=dk(today0());if(!title&&!body.trim()){toast('Escreva algo antes de salvar.');return;}if(!title)title=body.split('\n')[0].replace(/^#+\s*/,'').substring(0,50)||'Sem título';const items=getLib();if(_neId){const idx=items.findIndex(l=>l.id===_neId);if(idx>=0){items[idx].title=title;items[idx].body=body;items[idx].tags=tags;items[idx].updated=today;}}else{items.unshift({id:uid(),title,body,type,tags,date:today,updated:today});}setLib(items);closeNoteEditor();render();toast('✓ Nota salva!');if(_graphOpen)setTimeout(renderGraph,200);} function neFormat(cmd){document.execCommand(cmd,false,null);_neBody&&_neBody.focus();neUpdateToolbar();} function neBlock(tag){document.execCommand('formatBlock',false,tag);_neBody&&_neBody.focus();} function neInsertList(){document.execCommand('insertUnorderedList',false,null);_neBody&&_neBody.focus();} function neInsertCode(){const sel=window.getSelection();document.execCommand('insertHTML',false,''+(sel&&sel.toString()?escHtml(sel.toString()):'código')+'');_neBody&&_neBody.focus();} function neInsertDate(){document.execCommand('insertHTML',false,''+dk(today0())+' ');_neBody&&_neBody.focus();} function neInsertWikiPrompt(){const t=prompt('Nome da nota:');if(!t)return;neInsertWikiLink(t);} function neInsertWikiLink(title){const f=getLib().find(l=>(l.title||'').toLowerCase()===title.toLowerCase());document.execCommand('insertHTML',false,''+escHtml(title)+' ');_neBody&&_neBody.focus();} function neUpdateToolbar(){['bold','italic','underline'].forEach(cmd=>{const btn=document.querySelector('.tb-btn[onclick*="\''+cmd+'\'"]');if(btn)btn.classList.toggle('active',document.queryCommandState(cmd));});} function neOnInput(){clearTimeout(_neLinkTimer);_neLinkTimer=setTimeout(neCheckWikiLink,150);} function neCheckWikiLink(){const sel=window.getSelection();if(!sel||!sel.anchorNode)return;const text=sel.anchorNode.textContent||'';const match=text.slice(0,sel.anchorOffset).match(/\[\[([^\]\[]*)$/);if(!match){neHideAC();return;}neShowAC(match[1]);} function neShowAC(q){const ac=document.getElementById('link-autocomplete');if(!ac)return;const sel=window.getSelection();if(!sel||!sel.getRangeAt){neHideAC();return;}const rect=sel.getRangeAt(0).getBoundingClientRect();ac.style.left=rect.left+'px';ac.style.top=(rect.bottom+6)+'px';ac.style.display='block';const libs=getLib().filter(l=>!q||(l.title||'').toLowerCase().includes(q.toLowerCase())).slice(0,6);const icons={nota:'📝',citacao:'💬',livro:'📚'};let h=libs.map(l=>'
'+(icons[l.type]||'📝')+''+escHtml(l.title||'Sem título')+'
').join('');if(q)h+='
+ Criar "'+escHtml(q)+'"
';if(!h){neHideAC();return;}ac.innerHTML=h;const first=ac.querySelector('.lac-item');if(first)first.classList.add('sel');} function neACPick(title){neHideAC();const sel=window.getSelection();if(!sel||!sel.anchorNode)return;const node=sel.anchorNode,pos=sel.anchorOffset,text=node.textContent;const match=text.slice(0,pos).match(/\[\[([^\]\[]*)$/);if(match){const range=document.createRange();range.setStart(node,pos-match[0].length);range.setEnd(node,pos);range.deleteContents();sel.removeAllRanges();sel.addRange(range);}neInsertWikiLink(title);} function neHideAC(){const ac=document.getElementById('link-autocomplete');if(ac)ac.style.display='none';} function neOnKeydown(e){const ac=document.getElementById('link-autocomplete');if(ac&&ac.style.display!=='none'){const items=ac.querySelectorAll('.lac-item'),sel=ac.querySelector('.lac-item.sel'),idx=Array.from(items).indexOf(sel);if(e.key==='ArrowDown'){e.preventDefault();sel&&sel.classList.remove('sel');const n=items[Math.min(idx+1,items.length-1)];n&&n.classList.add('sel');return;}if(e.key==='ArrowUp'){e.preventDefault();sel&&sel.classList.remove('sel');const p=items[Math.max(idx-1,0)];p&&p.classList.add('sel');return;}if(e.key==='Enter'&&sel){e.preventDefault();sel.click();return;}if(e.key==='Escape'){neHideAC();return;}}if(e.key==='Tab'){e.preventDefault();document.execCommand('insertHTML',false,'    ');}if((e.ctrlKey||e.metaKey)&&e.key==='f'){e.preventDefault();noteSearchOpen();}} function neWikiOpen(id){if(id){closeNoteEditor();setTimeout(()=>openNoteEditor(id),100);}} function neMarkdownToHtml(md){if(!md)return '';let h=md.replace(/&/g,'&').replace(//g,'>').replace(/\[\[([^\]]+)\]\]/g,(m,t)=>{const f=getLib().find(l=>(l.title||'').toLowerCase()===t.toLowerCase());return ''+escHtml(t)+'';}).replace(/^### (.+)$/gm,'

$1

').replace(/^## (.+)$/gm,'

$1

').replace(/^# (.+)$/gm,'

$1

').replace(/\*\*([^*]+)\*\*/g,'$1').replace(/\*([^*]+)\*/g,'$1').replace(/^> (.+)$/gm,'
$1
').replace(/`([^`]+)`/g,'$1').replace(/^- (.+)$/gm,'
  • $1
  • ').replace(/\n\n/g,'

    ').replace(/\n/g,'
    ');return '

    '+h+'

    ';} function neHtmlToMarkdown(h){return h.replace(/]*>(.*?)<\/h1>/gi,'# $1\n').replace(/]*>(.*?)<\/h2>/gi,'## $1\n').replace(/]*>(.*?)<\/h3>/gi,'### $1\n').replace(/]*>(.*?)<\/strong>/gi,'**$1**').replace(/]*>(.*?)<\/b>/gi,'**$1**').replace(/]*>(.*?)<\/em>/gi,'*$1*').replace(/]*>(.*?)<\/i>/gi,'*$1*').replace(/]*>(.*?)<\/blockquote>/gi,'> $1\n').replace(/]*>(.*?)<\/code>/gi,'`$1`').replace(/]*>(.*?)<\/li>/gi,'- $1\n').replace(/]*>|<\/ul>/gi,'').replace(/]*class="wiki-link[^"]*"[^>]*>([^<]+)<\/a>/gi,'[[$1]]').replace(//gi,'\n').replace(/<\/p>/gi,'\n').replace(/]*>/gi,'').replace(/]*>/gi,'\n').replace(/<\/div>/gi,'').replace(/<[^>]+>/g,'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/ /g,' ').replace(/\n{3,}/g,'\n\n').trim();} function toggleGraph(){_graphOpen?closeGraph():openGraph();} function openGraph(){_graphOpen=true;document.getElementById('graph-panel').classList.add('open');const btn=document.getElementById('graph-toggle-btn');if(btn)btn.classList.add('active');renderGraph();} function closeGraph(){_graphOpen=false;document.getElementById('graph-panel').classList.remove('open');const btn=document.getElementById('graph-toggle-btn');if(btn)btn.classList.remove('active');if(_graphSim){_graphSim.stop();_graphSim=null;}} function renderGraph(){const svg=document.getElementById('graph-svg');if(!svg)return;if(typeof d3==='undefined'){const s=document.createElement('script');s.src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js';s.onload=()=>renderGraph();document.head.appendChild(s);return;}const libs=getLib();const W=svg.clientWidth||340,H=svg.clientHeight||window.innerHeight-60;const nodes=libs.map(l=>({id:l.id,title:l.title||'Sem título',type:l.type||'nota'}));const nodeMap={};libs.forEach(l=>{nodeMap[(l.title||'').toLowerCase()]=l.id;});const links=[];libs.forEach(l=>{[...(l.body||'').matchAll(/\[\[([^\]]+)\]\]/g)].forEach(m=>{const tid=nodeMap[m[1].toLowerCase()];if(tid&&tid!==l.id)links.push({source:l.id,target:tid});});});d3.select(svg).selectAll('*').remove();const svgEl=d3.select(svg).attr('width',W).attr('height',H);const g=svgEl.append('g');svgEl.call(d3.zoom().scaleExtent([.2,4]).on('zoom',e=>g.attr('transform',e.transform)));const typeColor={nota:'var(--accent)',citacao:'var(--blue)',livro:'#0f9d58'};const connCount={};nodes.forEach(n=>connCount[n.id]=0);links.forEach(l=>{connCount[l.source]=(connCount[l.source]||0)+1;connCount[l.target]=(connCount[l.target]||0)+1;});const link=g.append('g').selectAll('line').data(links).join('line').attr('stroke','var(--border2)').attr('stroke-width',1.5).attr('stroke-opacity',.7);const node=g.append('g').selectAll('g').data(nodes).join('g').style('cursor','pointer').call(d3.drag().on('start',(e,d)=>{if(!e.active)_graphSim.alphaTarget(.3).restart();d.fx=d.x;d.fy=d.y;}).on('drag',(e,d)=>{d.fx=e.x;d.fy=e.y;}).on('end',(e,d)=>{if(!e.active)_graphSim.alphaTarget(0);d.fx=null;d.fy=null;})).on('click',(e,d)=>openNoteEditor(d.id)).on('mouseover',(e,d)=>{const tt=document.getElementById('graph-tooltip');if(tt){tt.textContent=d.title;tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';}}).on('mouseout',()=>{const tt=document.getElementById('graph-tooltip');if(tt)tt.style.display='none';});node.append('circle').attr('r',d=>6+Math.min((connCount[d.id]||0)*2,12)).attr('fill',d=>typeColor[d.type]||'var(--accent)').attr('fill-opacity',.9).attr('stroke','var(--bg)').attr('stroke-width',2.5);node.append('text').text(d=>d.title.length>18?d.title.substring(0,16)+'…':d.title).attr('x',d=>10+Math.min((connCount[d.id]||0)*2,12)).attr('y',4).attr('font-size','10px').attr('fill','var(--text2)').attr('font-family','inherit');_graphSim=d3.forceSimulation(nodes).force('link',d3.forceLink(links).id(d=>d.id).distance(90)).force('charge',d3.forceManyBody().strength(-150)).force('center',d3.forceCenter(W/2,H/2)).force('collision',d3.forceCollide().radius(28)).on('tick',()=>{link.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);node.attr('transform',d=>'translate('+d.x+','+d.y+')');});const ce=document.getElementById('graph-node-count');if(ce)ce.textContent=nodes.length+' notas · '+links.length+' links';} function toggleActivityPanel(){_activityOpen?closeActivityPanel():openActivityPanel();} function openActivityPanel(){_activityOpen=true;const p=document.getElementById('activity-panel');if(p)p.classList.add('open');const btn=document.getElementById('activity-bell-btn');if(btn)btn.classList.add('active');const dot=document.getElementById('bell-dot');if(dot)dot.classList.remove('show');localStorage.setItem('ffos_activity_seen',Date.now().toString());renderActivityPanel();} function closeActivityPanel(){_activityOpen=false;const p=document.getElementById('activity-panel');if(p)p.classList.remove('open');const btn=document.getElementById('activity-bell-btn');if(btn)btn.classList.remove('active');} function renderActivityPanel(){const body=document.getElementById('activity-panel-body');if(!body)return;const todayStr=dk(today0());const yd=new Date(today0());yd.setDate(yd.getDate()-1);const ydStr=dk(yd);const items=[];getTasks().forEach(t=>{const d=t.updated||t.date||'';if(d>=ydStr)items.push({type:'task',id:t.id,name:t.name,meta:t.done?'Concluída':(t.date?'Vence '+t.date:'Sem data'),badge:t.done?'done':(d===todayStr?'new':'edited'),badgeText:t.done?'✓ Feita':(d===todayStr?'Hoje':'Ontem'),icon:'✓',iconClass:'task',date:d});});getLib().forEach(l=>{const d=l.updated||l.date||'';if(d>=ydStr){const icons={nota:'📝',citacao:'💬',livro:'📚'};const labels={nota:'Nota',citacao:'Citação',livro:'Livro'};items.push({type:'lib',id:l.id,name:l.title||'Sem título',meta:(labels[l.type]||'Nota')+' · '+d,badge:d===todayStr?'new':'edited',badgeText:d===todayStr?'Hoje':'Ontem',icon:icons[l.type]||'📝',iconClass:'note',date:d});}});getRoutines().forEach(r=>{if(localStorage.getItem('routine_'+r.id+'_'+todayStr))items.push({type:'routine',id:r.id,name:r.name,meta:'Rotina · Concluída hoje',badge:'done',badgeText:'✓ Feita',icon:'↻',iconClass:'routine',date:todayStr});});getProjects().forEach(p=>{const d=p.updated||'';if(d>=ydStr)items.push({type:'project',id:p.id,name:p.name,meta:'Projeto · '+p.status,badge:d===todayStr?'new':'edited',badgeText:d===todayStr?'Hoje':'Ontem',icon:'▣',iconClass:'project',date:d});});items.sort((a,b)=>b.date>a.date?1:-1);if(!items.length){body.innerHTML='
    Nenhuma atividade recente
    ';return;}function ri(arr){return arr.map(i=>'
    '+i.icon+'
    '+escHtml(i.name)+'
    '+escHtml(i.meta)+'
    '+i.badgeText+'
    ').join('');}const ti=items.filter(i=>i.date===todayStr),yi=items.filter(i=>i.date===ydStr);let h='';if(ti.length)h+=''+ri(ti);if(yi.length)h+=''+ri(yi);body.innerHTML=h;} function actItemClick(type,id){closeActivityPanel();setTimeout(()=>{if(type==='task'){goTo('tasks');setTimeout(()=>openEditTask(id),150);}else if(type==='lib'){goTo('library');setTimeout(()=>openEditLib(id),150);}else if(type==='routine'){goTo('routines');}else if(type==='project'){goTo('projects');setTimeout(()=>openEditProject(id),150);}},200);} function checkActivityBell(){const todayStr=dk(today0());const yd=new Date(today0());yd.setDate(yd.getDate()-1);const ydStr=dk(yd);let hasNew=false;getTasks().forEach(t=>{if((t.updated||t.date||'')>=ydStr)hasNew=true;});getLib().forEach(l=>{if((l.updated||l.date||'')>=ydStr)hasNew=true;});const dot=document.getElementById('bell-dot');if(dot&&hasNew)dot.classList.add('show');} function toggleFabMenu(){_fabOpen?closeFabMenu():openFabMenu();} function openFabMenu(){_fabOpen=true;document.getElementById('fab-menu').classList.add('open');document.getElementById('fab-backdrop').classList.add('show');} function closeFabMenu(){_fabOpen=false;const m=document.getElementById('fab-menu');const b=document.getElementById('fab-backdrop');if(m)m.classList.remove('open');if(b)b.classList.remove('show');} function hideSplash(){const s=document.getElementById('splash-screen');if(s){s.classList.add('hide');setTimeout(()=>s.remove(),400);}} function startClock(){function tick(){const el=document.getElementById('today-clock');if(el){const now=new Date();el.textContent=now.toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit',second:'2-digit'});}}tick();setInterval(tick,1000);} function renderRoutineProgress(){const el=document.getElementById('routine-progress-wrap');if(!el)return;const routines=getRoutines();if(!routines.length){el.style.display='none';return;}const todayStr=dk(today0());const done=routines.filter(r=>localStorage.getItem('routine_'+r.id+'_'+todayStr)).length;const pct=Math.round(done/routines.length*100);el.innerHTML='
    Rotinas de hoje'+done+'/'+routines.length+' ('+pct+'%)
    ';el.style.display='block';} async function fetchWeatherInline(){const el=document.getElementById('today-weather-inline');if(!el||!navigator.geolocation)return;try{navigator.geolocation.getCurrentPosition(async pos=>{const{latitude:lat,longitude:lon}=pos.coords;const r=await fetch('https://api.open-meteo.com/v1/forecast?latitude='+lat+'&longitude='+lon+'¤t_weather=true');const d=await r.json();const w=d.current_weather;const icons={0:'☀️',1:'🌤',2:'⛅',3:'☁️',45:'🌫',51:'🌦',61:'🌧',71:'❄️',80:'🌦',95:'⛈'};el.innerHTML=''+(icons[w.weathercode]||'🌡')+''+Math.round(w.temperature)+'°C';});}catch(e){}} function initOfflineIndicator(){const banner=document.getElementById('offline-banner');if(!banner)return;function update(){banner.classList.toggle('show',!navigator.onLine);}window.addEventListener('online',update);window.addEventListener('offline',update);update();} let _libSort='updated'; function setLibSort(s,btn){_libSort=s;document.querySelectorAll('.lib-sort-btn').forEach(b=>b.classList.remove('active'));if(btn)btn.classList.add('active');renderLibrary();} function applyLibSort(items){const s=items.slice();if(_libSort==='updated')s.sort((a,b)=>(b.updated||b.date||'')>(a.updated||a.date||'')?1:-1);else if(_libSort==='created')s.sort((a,b)=>(b.date||'')>(a.date||'')?1:-1);else if(_libSort==='alpha')s.sort((a,b)=>(a.title||'')>(b.title||'')?1:-1);return s;} function libPreview(body){if(!body)return '';return body.replace(/\[\[([^\]]+)\]\]/g,'$1').replace(/[#*_`>]/g,'').trim().substring(0,100);} function dueDateLabel(dateStr){if(!dateStr)return '';const today=dk(today0());if(dateStr'+days+'d atraso';}if(dateStr===today)return 'Hoje';const days=Math.floor((new Date(dateStr)-new Date(today))/86400000);if(days===1)return 'Amanhã';if(days<=3)return 'em '+days+'d';return ''+dateStr+'';} let _noteSearchMatches=[],_noteSearchIdx=0; function noteSearchOpen(){const bar=document.getElementById('note-search-bar');if(bar){bar.classList.add('open');document.getElementById('note-search-input').focus();}} function noteSearchClose(){const bar=document.getElementById('note-search-bar');if(bar)bar.classList.remove('open');const content=document.getElementById('note-editor-content');if(content)content.innerHTML=content.innerHTML.replace(/(.*?)<\/mark>/g,'$1');const cnt=document.getElementById('note-search-count');if(cnt)cnt.textContent='';_noteSearchMatches=[];} function noteSearchRun(q){const content=document.getElementById('note-editor-content');if(!content)return;content.innerHTML=content.innerHTML.replace(/(.*?)<\/mark>/g,'$1');if(!q||!q.trim())return;const escaped=q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');content.innerHTML=content.innerHTML.replace(new RegExp('('+escaped+')','gi'),'$1');_noteSearchMatches=Array.from(content.querySelectorAll('mark.search-hl'));_noteSearchIdx=0;noteSearchHighlight();const cnt=document.getElementById('note-search-count');if(cnt)cnt.textContent=_noteSearchMatches.length?'1/'+_noteSearchMatches.length:'0';} function noteSearchNav(dir){if(!_noteSearchMatches.length)return;_noteSearchMatches[_noteSearchIdx].classList.remove('current');_noteSearchIdx=(_noteSearchIdx+dir+_noteSearchMatches.length)%_noteSearchMatches.length;noteSearchHighlight();const cnt=document.getElementById('note-search-count');if(cnt)cnt.textContent=(_noteSearchIdx+1)+'/'+_noteSearchMatches.length;} function noteSearchHighlight(){if(!_noteSearchMatches[_noteSearchIdx])return;_noteSearchMatches[_noteSearchIdx].classList.add('current');_noteSearchMatches[_noteSearchIdx].scrollIntoView({block:'center'});} function exportNotePDF(){const title=(document.getElementById('editor-title')||{value:'Nota'}).value||'Nota';const content=document.getElementById('note-editor-content');if(!content)return;const w=window.open('','_blank');if(!w){toast('Permita popups para exportar PDF.');return;}w.document.write('Fast Fluent OS v2.0617.1631

    '+escHtml(title)+'

    '+content.innerHTML+'');w.document.close();setTimeout(function(){w.print();},500);} let _vaRecording=false,_vaMediaRec=null,_vaChunks=[],_vaSpeaking=false; function openVoiceAssistant(){const ov=document.getElementById('voice-assistant-overlay');if(ov)ov.classList.add('open');vaClear();} function closeVoiceAssistant(){const ov=document.getElementById('voice-assistant-overlay');if(ov)ov.classList.remove('open');vaStopMic();vaStopSpeaking();} function openVoiceDiary(){openVoiceAssistant();} function vaStatus(text,cls){const el=document.getElementById('va-status');if(!el)return;el.textContent=text;el.className='va-status'+(cls?' '+cls:'');} function vaWave(on){const wf=document.getElementById('va-waveform');if(wf)wf.classList.toggle('listening',on);const btn=document.getElementById('va-mic-btn');if(btn){btn.classList.remove('listening','speaking');if(on)btn.classList.add('listening');}} function vaClear(){const t=document.getElementById('va-transcript');const r=document.getElementById('va-response');const sb=document.getElementById('va-speak-btn');const ab=document.getElementById('va-response-actions');if(t)t.classList.remove('show');if(r)r.classList.remove('show');if(sb)sb.style.display='none';if(ab)ab.innerHTML='';vaStatus('Toque no microfone para falar','');vaStopSpeaking();const btn=document.getElementById('va-mic-btn');if(btn)btn.classList.remove('listening','speaking');vaWave(false);} function vaToggleMic(){_vaRecording?vaStopMic():vaStartMic();} async function vaStartMic(){const key=localStorage.getItem('ffos_groq_key');if(!key){toast('Configure a Groq API Key em Configurações.');return;}try{const stream=await navigator.mediaDevices.getUserMedia({audio:true});_vaChunks=[];_vaMediaRec=new MediaRecorder(stream,{mimeType:'audio/webm'});_vaMediaRec.ondataavailable=e=>{if(e.data.size>0)_vaChunks.push(e.data);};_vaMediaRec.onstop=async()=>{stream.getTracks().forEach(t=>t.stop());await vaProcessAudio(new Blob(_vaChunks,{type:'audio/webm'}));};_vaMediaRec.start();_vaRecording=true;vaWave(true);vaStatus('Ouvindo... toque para parar','listening');setTimeout(()=>{if(_vaRecording)vaStopMic();},30000);}catch(err){toast('Microfone não disponível.');console.error(err);}} function vaStopMic(){if(_vaMediaRec&&_vaRecording){_vaMediaRec.stop();_vaRecording=false;vaWave(false);vaStatus('Processando...','thinking');}} async function vaProcessAudio(blob){const key=localStorage.getItem('ffos_groq_key');vaStatus('Transcrevendo...','thinking');try{const fd=new FormData();fd.append('file',blob,'audio.webm');fd.append('model','whisper-large-v3');fd.append('language','pt');const tr=await fetch('https://api.groq.com/openai/v1/audio/transcriptions',{method:'POST',headers:{'Authorization':'Bearer '+key},body:fd});const td=await tr.json();const text=td.text||'';if(!text.trim()){vaStatus('Não entendi. Tente novamente.','');return;}const tEl=document.getElementById('va-transcript');const tTxt=document.getElementById('va-transcript-text');if(tEl)tEl.classList.add('show');if(tTxt)tTxt.textContent=text;vaStatus('Pensando...','thinking');await vaAskGroq(text,key);}catch(err){vaStatus('Erro ao processar.','');console.error(err);}} async function vaAskGroq(question,key){const todayStr=dk(today0());const cal=(_gcalEvents||[]).filter(ev=>dk(new Date(ev.start.dateTime||ev.start.date))===todayStr).map(ev=>{const t=ev.start.dateTime?new Date(ev.start.dateTime).toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'}):'Dia todo';return t+' - '+(ev.summary||'');}).join('; ');const tasks=getTasks().filter(t=>!t.done).slice(0,20).map(t=>t.name+(t.date?' ['+t.date+']':'')).join('; ');const routines=getRoutines().map(r=>r.name).join(', ');const projects=getProjects().filter(p=>p.status==='ativo').map(p=>p.name).join(', ');const sys='Você é o assistente pessoal de voz de Tiago, dono da Fast Fluent Idiomas no Rio de Janeiro. Hoje é '+todayStr+'. Agenda: '+(cal||'nenhum evento')+'. Tarefas: '+(tasks||'nenhuma')+'. Rotinas: '+(routines||'nenhuma')+'. Projetos: '+(projects||'nenhum')+'. Responda em português, de forma natural e concisa. Máximo 2 parágrafos.';try{const res=await fetch('https://api.groq.com/openai/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+key},body:JSON.stringify({model:'llama-3.3-70b-versatile',max_tokens:250,messages:[{role:'system',content:sys},{role:'user',content:question}]})});const data=await res.json();const answer=data.choices&&data.choices[0]?data.choices[0].message.content:'';if(!answer){vaStatus('Sem resposta.','');return;}const rEl=document.getElementById('va-response');const rTxt=document.getElementById('va-response-text');const sb=document.getElementById('va-speak-btn');if(rEl)rEl.classList.add('show');if(rTxt)rTxt.textContent=answer;if(sb)sb.style.display='flex';vaCheckActions(question);vaStatus('Respondendo...','speaking');const btn=document.getElementById('va-mic-btn');if(btn){btn.classList.remove('listening');btn.classList.add('speaking');}await vaSpeak(answer);vaStatus('Toque para continuar','');if(btn)btn.classList.remove('speaking');}catch(err){vaStatus('Erro ao conectar com a IA.','');console.error(err);}} function vaSpeak(text){return new Promise(resolve=>{const synth=window.speechSynthesis;if(!synth||!text){resolve();return;}synth.cancel();const utt=new SpeechSynthesisUtterance(text);utt.lang='pt-BR';utt.rate=1.05;utt.pitch=1;const voices=synth.getVoices();const v=voices.find(v=>v.lang.startsWith('pt'))||null;if(v)utt.voice=v;utt.onend=resolve;utt.onerror=resolve;_vaSpeaking=true;synth.speak(utt);});} function vaStopSpeaking(){if(window.speechSynthesis)window.speechSynthesis.cancel();_vaSpeaking=false;} function vaCheckActions(question){const actEl=document.getElementById('va-response-actions');if(!actEl)return;const q=question.toLowerCase();let btns='';if(/adiciona|cria|criar|lembra|tarefa/i.test(q))btns+='';if(/nota|anota|salva|escreve/i.test(q))btns+='';actEl.innerHTML=btns;} function vaCreateTask(){const tTxt=document.getElementById('va-transcript-text');closeVoiceAssistant();setTimeout(()=>{openAddTask();setTimeout(()=>{const n=document.getElementById('t-name');if(n&&tTxt)n.value=tTxt.textContent;},150);},300);} function vaCreateNote(){const rTxt=document.getElementById('va-response-text');closeVoiceAssistant();setTimeout(()=>{openNoteEditor(null,'nota');setTimeout(()=>{const c=document.getElementById('note-editor-content');if(c&&rTxt)c.innerHTML='

    '+escHtml(rTxt.textContent)+'

    ';},200);},300);} ction vaCreateNote(){const rTxt=document.getElementById('va-response-text');closeVoiceAssistant();setTimeout(()=>{openNoteEditor(null,'nota');setTimeout(()=>{const c=document.getElementById('note-editor-content');if(c&&rTxt)c.innerHTML='

    '+escHtml(rTxt.textContent)+'

    ';},200);},300);}