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>
187 lines
7.8 KiB
JavaScript
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, '<')}</div>`
|
|
).join('');
|
|
}
|
|
|
|
$('cal-caldav-settings')?.addEventListener('click', openCaldavSettings);
|