Files
hyprcrd/public/js/modules/caldav.js
T
thandClaude Opus 4.6 1d6499e5dd extract sync orchestration into sync.js, unify on sync.tick heartbeat
caldav.js was carrying the core sync timer, autosync toggle, relay
settings and every poller hook alongside its actual CalDAV/calendar
config concerns. Split out:

- New sync.js owns the single background timer, header Sync button,
  autosync pause toggle, configurable interval, and emits `sync.tick`
  so plugins can fan out without their own setIntervals.
- collab-buckets + chat now subscribe to sync.tick instead of running
  their own pollers, so the manual Sync button refreshes them too and
  the Autosync pause actually pauses everything.
- Replace the number input for sync interval with a stepped slider
  (5s..1h snap points) and a live label, bumped to 1.9.2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:02:44 +02:00

187 lines
7.8 KiB
JavaScript

// ── CalDAV Settings + Calendar Config ──
//
// Genuinely CalDAV-scoped concerns only:
// * Settings panel (connection URL / user / password, Test + Save)
// * Per-calendar color/icon configuration (persisted in localStorage)
//
// Sync orchestration (triggerSync, background poll, autosync toggle,
// sync-interval, relay settings, sync.tick hook) lives in sync.js.
import { $, esc, api, getCsrf } from './util.js';
const CAL_ICONS = [
{ id: 'calendar-dots', label: 'calendar' },
{ id: 'briefcase', label: 'work' },
{ id: 'house', label: 'home' },
{ id: 'heart', label: 'personal' },
{ id: 'cake', label: 'birthday' },
{ id: 'graduation-cap', label: 'education' },
{ id: 'airplane', label: 'travel' },
{ id: 'basketball', label: 'sport' },
{ id: 'first-aid', label: 'health' },
{ id: 'star', label: 'important' },
{ id: 'users', label: 'team' },
{ id: 'phone', label: 'calls' },
];
const CAL_COLORS = ['#38bdf8', '#8b5cf6', '#fb7185', '#fbbf24', '#4ade80', '#fb923c', '#22d3ee', '#f472b6', '#a3e635', '#94a3b8'];
let calendarConfigs = {}; // slug → { color, icon }
const CAL_CONFIG_KEY = 'hyprcrd-cal-configs';
const settingsOverlay = $('settings-overlay');
const urlInput = $('caldav-url');
const userInput = $('caldav-user');
const passInput = $('caldav-pass');
const testStatus = $('caldav-test-status');
const syncStatus = $('caldav-sync-status');
const logEl = $('caldav-log');
export function openCaldavSettings() {
// Open settings overlay on sync tab
settingsOverlay?.classList.add('open');
settingsOverlay?.querySelectorAll('.settings-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === 'caldav'));
settingsOverlay?.querySelectorAll('[id^="settings-panel-"]').forEach(p => p.classList.toggle('hidden', p.id !== 'settings-panel-caldav'));
loadSettings();
}
export function closeCaldavSettings() {
settingsOverlay?.classList.remove('open');
}
function loadCalConfigs() {
try { calendarConfigs = JSON.parse(localStorage.getItem(CAL_CONFIG_KEY) || '{}'); } catch { calendarConfigs = {}; }
}
function saveCalConfigs() {
localStorage.setItem(CAL_CONFIG_KEY, JSON.stringify(calendarConfigs));
}
export function getCalConfig(slug) {
loadCalConfigs();
return calendarConfigs[slug] || null;
}
export function getAllCalConfigs() {
loadCalConfigs();
return calendarConfigs;
}
function renderCalList(calendars) {
const container = $('caldav-cal-list');
const wrap = $('caldav-calendars');
if (!container || !wrap || !calendars?.length) return;
loadCalConfigs();
wrap.classList.remove('hidden');
container.innerHTML = calendars.map(cal => {
const slug = cal.name.toLowerCase().replace(/[^a-z0-9_-]/g, '');
const cfg = calendarConfigs[slug] || {};
const activeColor = cfg.color || '#94a3b8';
const activeIcon = cfg.icon || 'calendar-dots';
return `<div class="flex items-center gap-2 py-1" data-cal-slug="${slug}">
<span style="width:10px;height:10px;border-radius:2px;background:${activeColor};flex-shrink:0;"></span>
<span class="font-pixel text-[10px] text-t-bright flex-1">${esc(cal.name)}</span>
<div class="flex gap-1">
${CAL_COLORS.map(c => `<button class="cal-cfg-color" data-color="${c}" style="width:14px;height:14px;border-radius:2px;background:${c};border:2px solid ${c === activeColor ? 'var(--t-bright)' : 'transparent'};cursor:pointer;"></button>`).join('')}
</div>
<div class="flex gap-1 ml-1">
${CAL_ICONS.slice(0, 6).map(ic => `<button class="cal-cfg-icon" data-icon="${ic.id}" data-tip="${ic.label}" style="font-size:12px;cursor:pointer;padding:1px;border-radius:2px;${ic.id === activeIcon ? 'color:var(--t-accent);' : 'color:var(--t-dim);'}"><i class="ph ph-${ic.id}"></i></button>`).join('')}
</div>
</div>`;
}).join('');
container.querySelectorAll('.cal-cfg-color').forEach(btn => {
btn.addEventListener('click', () => {
const slug = btn.closest('[data-cal-slug]').dataset.calSlug;
if (!calendarConfigs[slug]) calendarConfigs[slug] = {};
calendarConfigs[slug].color = btn.dataset.color;
saveCalConfigs();
renderCalList(calendars);
});
});
container.querySelectorAll('.cal-cfg-icon').forEach(btn => {
btn.addEventListener('click', () => {
const slug = btn.closest('[data-cal-slug]').dataset.calSlug;
if (!calendarConfigs[slug]) calendarConfigs[slug] = {};
calendarConfigs[slug].icon = btn.dataset.icon;
saveCalConfigs();
renderCalList(calendars);
});
});
}
export async function loadSettings() {
const s = await api('api/caldav/settings');
if (!s) return;
if (urlInput) urlInput.value = s.url || '';
if (userInput) userInput.value = s.username || '';
if (passInput) passInput.value = '';
if (passInput && s.has_password) passInput.placeholder = '(saved)';
if (syncStatus) syncStatus.textContent = s.last_sync ? `last sync: ${s.last_sync}` : '';
// Load calendar list if configured
if (s.url && s.has_password) {
const test = await fetch('api/caldav/test', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
body: '{}',
}).then(r => r.json()).catch(() => null);
if (test?.calendars) renderCalList(test.calendars);
}
}
function setCaldavTestStatus(text, kind = 'info') {
if (!testStatus) return;
if (!text) {
testStatus.classList.add('hidden');
testStatus.textContent = '';
testStatus.style.borderColor = '';
testStatus.style.color = '';
return;
}
testStatus.classList.remove('hidden');
testStatus.textContent = text;
if (kind === 'error') {
testStatus.style.borderColor = 'var(--t-danger)';
testStatus.style.color = 'var(--t-danger)';
} else if (kind === 'success') {
testStatus.style.borderColor = 'var(--t-accent)';
testStatus.style.color = 'var(--t-accent)';
} else {
testStatus.style.borderColor = 'var(--t-border)';
testStatus.style.color = 'var(--t-muted)';
}
}
$('caldav-test')?.addEventListener('click', async () => {
setCaldavTestStatus('testing...');
const body = { url: urlInput.value, username: userInput.value };
if (passInput.value) body.password = passInput.value;
const res = await fetch('api/caldav/test', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
body: JSON.stringify(body),
}).then(r => r.json()).catch(() => ({ ok: false, error: 'network error' }));
if (res.ok) {
const names = (res.calendars || []).map(c => c.name).join(', ');
setCaldavTestStatus(`connected (${names || 'no calendars'})`, 'success');
if (res.calendars) renderCalList(res.calendars);
} else {
setCaldavTestStatus(res.error || 'failed', 'error');
}
showConnectionLog(res.log);
});
$('caldav-save')?.addEventListener('click', async () => {
const body = { url: urlInput.value, username: userInput.value, enabled: true };
if (passInput.value) body.password = passInput.value;
await api('api/caldav/settings', { method: 'POST', body });
testStatus.textContent = 'saved';
testStatus.style.color = 'var(--t-accent)';
});
function showConnectionLog(lines) {
if (!logEl || !lines?.length) return;
logEl.classList.remove('hidden');
logEl.innerHTML = lines.map(l =>
`<div class="${l.startsWith('ERROR') || l.startsWith('EXCEPTION') ? 'text-t-danger' : l.startsWith('OK') ? 'text-t-accent' : 'text-t-dim'}">${l.replace(/</g, '&lt;')}</div>`
).join('');
}
$('cal-caldav-settings')?.addEventListener('click', openCaldavSettings);