// Links tab: your own bookmarks, grouped. Saved with the other per-person settings (see app.js: store). // ---- Links: your own bookmarks, grouped, saved with your other settings (private to you, every device) const safeUrl = s => { let t = String(s || '').trim(); if (!t) return ''; if (!/^[a-z][a-z0-9+.-]*:/i.test(t)) t = 'https://' + t; try { const u = new URL(t); return /^https?:$/.test(u.protocol) ? u.href : ''; } catch { return ''; } }; const linkList = () => { const l = store.get('links', []); return Array.isArray(l) ? l.filter(x => x && safeUrl(x.url)) : []; }; const LINK_COLORS = ['#4f7cff', '#2f9e6e', '#e0742b', '#d9453a', '#8a5cf0', '#e5a00d', '#0f9d9a', '#c2417a']; const linkColor = l => LINK_COLORS[[...String(l.title || l.url)].reduce((n, c) => n + c.charCodeAt(0), 0) % LINK_COLORS.length]; /** Links that come with the hub rather than from you (Podcast Admin, for people who may use it). They cannot be edited or deleted. */ const builtinLinks = () => allowed('verse') && cfg.verse && safeUrl(cfg.verse) ? [{ id: 'builtin-verse', title: 'Podcast Admin', url: safeUrl(cfg.verse), group: 'Podcast', builtin: true }] : []; const allLinks = () => [...builtinLinks(), ...linkList()]; let linkQuery = ''; function viewLinks() { const all = allLinks(), q = linkQuery.trim().toLowerCase(); const shown = q ? all.filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(q)) : all; const groups = [...new Set(shown.map(l => l.group || ''))].sort((a, b) => (a === '') - (b === '') || a.localeCompare(b)); $('#view').innerHTML = `

Links

${!ME || ME.tabs.includes('links') ? `` : ''}
${all.length ? '' : '

No links yet. Add the sites you use most; they are saved on your server and show up on every device.

'} ${all.length && !shown.length ? '

Nothing matches.

' : ''} ${groups.map(g => `${g ? `

${esc(g)}

` : (groups.length > 1 ? '

Other

' : '')}
${shown.filter(l => (l.group || '') === g).map(l => ` `).join('')}
`).join('')}
`; $('#lq').oninput = e => { linkQuery = e.target.value; const p = e.target.selectionStart; viewLinks(); const n = $('#lq'); n.focus(); n.setSelectionRange(p, p); }; if ($('#ladd')) $('#ladd').onclick = () => linkSheet(); document.querySelectorAll('[data-edit]').forEach(b => b.onclick = () => linkSheet(all.find(l => l.id === b.dataset.edit))); } function linkSheet(link) { const groups = [...new Set(linkList().map(l => l.group).filter(Boolean))]; const el = openSheet(link ? 'Edit link' : 'Add a link', '', `
${groups.map(g => `
${link ? '' : ''}
`); if (el.querySelector('#sx')) el.querySelector('#sx').onclick = closeSheet; if (link) el.querySelector('#ld').onclick = () => confirmSheet('Delete link?', link.title, 'Delete', () => { store.set('links', linkList().filter(l => l.id !== link.id)); viewLinks(); }); el.querySelector('#lf').onsubmit = e => { e.preventDefault(); const url = safeUrl(el.querySelector('#lu').value); if (!url) { el.querySelector('#le').textContent = 'That is not a web address (it must be http or https).'; return; } const next = { id: link ? link.id : 'l' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), title: el.querySelector('#lt').value.trim().slice(0, 60), url, group: el.querySelector('#lg').value.trim().slice(0, 40) }; const rest = linkList(); store.set('links', link ? rest.map(l => l.id === link.id ? next : l) : [...rest, next]); closeSheet(); viewLinks(); }; }