// ── Shopping-list plugin (hyprcrd side) ──
//
// Tag a card with #einkauf or #shopping to make it eligible. Pick
// any number as "active" via the Shopping header button; each active
// card's body is pushed to the relay on every sync.tick and on every
// cards.reloaded so edits land on the phone within seconds. Pulls
// phone-side toggles back and PATCHes the local cards.
//
// No timestamp cursors — we diff the (uid, title, body) triple we
// last saw in sync with the relay, which is timezone-agnostic.
const hyprcrd = window.hyprcrd;
if (!hyprcrd) throw new Error('[shopping plugin] window.hyprcrd missing');
const $ = (id) => document.getElementById(id);
const ACTIVE_KEY = 'hyprcrd-shopping-active-ids';
const SYNCED_KEY = 'hyprcrd-shopping-synced';
const ORDER_KEY = 'hyprcrd-shopping-order';
function esc(s) {
return String(s ?? '')
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function hasEinkaufTag(card) {
const tags = (card.tags || '').split(',').map(s => s.trim());
return tags.includes('einkauf') || tags.includes('shopping');
}
// ── Active IDs (multi-select) ────────────────────────────────
function getActiveIds() {
try { return JSON.parse(localStorage.getItem(ACTIVE_KEY) || '[]'); } catch { return []; }
}
function setActiveIds(ids) {
localStorage.setItem(ACTIVE_KEY, JSON.stringify(ids.map(String)));
}
function isActive(id) {
return getActiveIds().includes(String(id));
}
function getActiveCards() {
const ids = getActiveIds();
if (!ids.length) return [];
const cards = window.state?.cards || [];
return ids.map(id => cards.find(c => String(c.id) === id)).filter(Boolean);
}
function getStoredOrder() {
try { return JSON.parse(localStorage.getItem(ORDER_KEY) || '[]'); } catch { return []; }
}
function setStoredOrder(ids) {
localStorage.setItem(ORDER_KEY, JSON.stringify(ids.map(String)));
}
function sortByOrder(cards) {
const order = getStoredOrder();
const idx = new Map(order.map((id, i) => [id, i]));
return [...cards].sort((a, b) => {
const ai = idx.has(String(a.id)) ? idx.get(String(a.id)) : 9999;
const bi = idx.has(String(b.id)) ? idx.get(String(b.id)) : 9999;
return ai - bi;
});
}
// ── Synced state (per card_uid) ──────────────────────────────
function getSyncedMap() {
try { return JSON.parse(localStorage.getItem(SYNCED_KEY) || '{}'); } catch { return {}; }
}
function setSyncedFor(uid, title, body, pos) {
const m = getSyncedMap();
m[String(uid)] = { title: title || '', body: body || '', pos: pos ?? -1 };
localStorage.setItem(SYNCED_KEY, JSON.stringify(m));
}
function removeSyncedFor(uid) {
const m = getSyncedMap();
delete m[String(uid)];
localStorage.setItem(SYNCED_KEY, JSON.stringify(m));
}
// ── Header button injection ──────────────────────────────────
function injectHeaderButton() {
if ($('shopping-btn')) return;
const header = document.querySelector('header#main-header');
if (!header) return;
const rows = header.querySelectorAll('.flex.items-center.gap-3');
const group = rows[rows.length - 1];
if (!group) return;
const btn = document.createElement('button');
btn.id = 'shopping-btn';
btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors';
btn.innerHTML = 'Shopping';
btn.addEventListener('click', openDialog);
group.insertBefore(btn, group.firstChild);
}
// ── Picker dialog ────────────────────────────────────────────
function openDialog() {
document.querySelector('.shopping-picker-overlay')?.remove();
const overlay = document.createElement('div');
overlay.className = 'shopping-picker-overlay';
overlay.style.cssText = 'position:fixed;inset:0;background:var(--t-overlay,rgba(0,0,0,0.6));z-index:200;display:flex;align-items:center;justify-content:center;padding:24px;';
const panel = document.createElement('div');
panel.style.cssText = 'background:var(--t-surface);border:1px solid var(--t-border);box-shadow:0 8px 32px rgba(0,0,0,0.6);width:100%;max-width:520px;max-height:80vh;display:flex;flex-direction:column;';
panel.innerHTML = `
Shopping lists
Cards tagged with #einkauf or
#shopping can be activated for the phone.
Check any number of lists; their items appear stacked under the
shopping tab in the relay UI.
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
const list = panel.querySelector('#shopping-picker-list');
const status = panel.querySelector('#shopping-picker-status');
const close = () => overlay.remove();
panel.querySelector('.shopping-picker-close').addEventListener('click', close);
panel.querySelector('.shopping-picker-cancel').addEventListener('click', close);
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
document.addEventListener('keydown', function escClose(e) {
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', escClose); }
});
const cards = window.state?.cards || [];
const candidates = sortByOrder(cards.filter(hasEinkaufTag));
const currentIds = new Set(getActiveIds());
const selected = new Set(currentIds);
if (!candidates.length) {
list.innerHTML = `
No cards tagged #einkauf or
#shopping yet. Create a card with checkbox items
and add one of those tags — it'll show up here.