calender stuff
This commit is contained in:
1 parent
8e78468743
commit
e0fca064ae
15 files changed
+1716
-67
No files matched your search
@@ -0,0 +1,242 @@
|
||||
// ── CalDAV Sync ──
|
||||
|
||||
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 overlay = $('caldav-settings');
|
||||
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() {
|
||||
overlay?.classList.add('open');
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
export function closeCaldavSettings() {
|
||||
overlay?.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}" title="${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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$('caldav-close')?.addEventListener('click', closeCaldavSettings);
|
||||
overlay?.addEventListener('click', e => { if (e.target === overlay) closeCaldavSettings(); });
|
||||
|
||||
$('caldav-test')?.addEventListener('click', async () => {
|
||||
testStatus.textContent = 'testing...';
|
||||
testStatus.style.color = 'var(--t-dim)';
|
||||
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(', ');
|
||||
testStatus.textContent = `connected (${names || 'no calendars'})`;
|
||||
testStatus.style.color = 'var(--t-accent)';
|
||||
if (res.calendars) renderCalList(res.calendars);
|
||||
} else {
|
||||
testStatus.textContent = res.error || 'failed';
|
||||
testStatus.style.color = 'var(--t-danger)';
|
||||
}
|
||||
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)';
|
||||
});
|
||||
|
||||
$('caldav-sync-now')?.addEventListener('click', () => triggerSync());
|
||||
|
||||
export async function triggerSync() {
|
||||
const btn = $('cal-sync');
|
||||
if (btn) btn.style.opacity = '0.5';
|
||||
if (syncStatus) { syncStatus.textContent = 'syncing...'; syncStatus.style.color = 'var(--t-dim)'; }
|
||||
|
||||
const res = await fetch('api/caldav/sync', {
|
||||
method: 'POST', headers: { 'X-CSRF-Token': getCsrf() },
|
||||
}).then(r => r.json()).catch(() => ({ ok: false, error: 'network error' }));
|
||||
|
||||
if (btn) btn.style.opacity = '';
|
||||
|
||||
if (res.ok) {
|
||||
const s = res.stats || {};
|
||||
const hasErrors = (res.log || []).some(l => l.status === 'error');
|
||||
const msg = `pushed:${s.pushed || 0} pulled:${s.pulled || 0} del:${s.deleted || 0}${s.conflicts ? ' conflicts:' + s.conflicts : ''}`;
|
||||
if (syncStatus) { syncStatus.textContent = msg; syncStatus.style.color = hasErrors ? 'var(--t-danger)' : 'var(--t-accent)'; }
|
||||
await window._reloadAll?.();
|
||||
showLog(res.log);
|
||||
showSyncNotification(s);
|
||||
} else {
|
||||
if (syncStatus) { syncStatus.textContent = res.error || 'sync failed'; syncStatus.style.color = 'var(--t-danger)'; }
|
||||
}
|
||||
}
|
||||
|
||||
function showLog(log) {
|
||||
if (!logEl || !log?.length) return;
|
||||
logEl.classList.remove('hidden');
|
||||
logEl.innerHTML = log.map(l =>
|
||||
`<div class="${l.status === 'error' ? 'text-t-danger' : 'text-t-dim'}">${l.dir} ${l.action} ${l.status}${l.detail ? ' — ' + l.detail : ''}</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
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);
|
||||
$('cal-sync')?.addEventListener('click', () => triggerSync());
|
||||
|
||||
// Auto-sync throttle
|
||||
let lastAutoSync = 0;
|
||||
export function autoSync() {
|
||||
const now = Date.now();
|
||||
if (now - lastAutoSync < 60000) return;
|
||||
lastAutoSync = now;
|
||||
triggerSync();
|
||||
}
|
||||
|
||||
// Background sync every 2 minutes
|
||||
function showSyncNotification(stats) {
|
||||
const s = stats || {};
|
||||
const parts = [];
|
||||
if (s.pulled) parts.push(`${s.pulled} received`);
|
||||
if (s.pushed) parts.push(`${s.pushed} sent`);
|
||||
if (s.deleted) parts.push(`${s.deleted} deleted`);
|
||||
if (!parts.length) return;
|
||||
|
||||
const note = document.createElement('div');
|
||||
note.style.cssText = 'position:fixed;bottom:60px;right:16px;z-index:200;background:var(--t-surface);border:1px solid var(--t-accent);border-radius:4px;padding:10px 14px;box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:300px;';
|
||||
note.innerHTML = `<div style="font-family:var(--t-font-pixel);font-size:10px;color:var(--t-accent);display:flex;align-items:center;gap:6px;"><i class="ph ph-cloud-arrow-down"></i>CalDAV: ${parts.join(', ')}</div>`;
|
||||
document.body.appendChild(note);
|
||||
setTimeout(() => { note.style.transition = 'opacity 0.3s'; note.style.opacity = '0'; setTimeout(() => note.remove(), 350); }, 4000);
|
||||
}
|
||||
|
||||
async function backgroundSync() {
|
||||
try {
|
||||
const res = await fetch('api/caldav/sync', {
|
||||
method: 'POST', headers: { 'X-CSRF-Token': getCsrf() },
|
||||
}).then(r => r.json()).catch(() => null);
|
||||
if (res?.ok) {
|
||||
const s = res.stats || {};
|
||||
if (s.pulled > 0 || s.pushed > 0 || s.deleted > 0) {
|
||||
await window._reloadAll?.();
|
||||
showSyncNotification(s);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Start background sync after app is ready
|
||||
let bgSyncInterval = null;
|
||||
export function startBackgroundSync() {
|
||||
if (bgSyncInterval) return;
|
||||
backgroundSync(); // first sync immediately
|
||||
bgSyncInterval = setInterval(backgroundSync, 120000); // then every 2 minutes
|
||||
}
|
||||
Reference in new issue
Block a user