Podcast Admin is now a built-in link on the Links page instead of its own menu entry
build-container / build (push) Successful in 19m43s
build-container / build (push) Successful in 19m43s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dae19176cd
commit
833342fced
6 files changed
+16
-14
No files matched your search
+5
-6
@@ -37,7 +37,7 @@ async function loadMe() {
|
||||
}
|
||||
// Signal is for desktop-class screens only (a window at least 900 px wide with a mouse or trackpad), and needs the server to have it set up.
|
||||
const sgMQ = matchMedia('(min-width: 900px) and (pointer: fine)');
|
||||
const allowed = id => id === 'planner' ? (allowed('calendar') || allowed('tasks')) : (!ME || ME.tabs.includes(id)) && (id !== 'signal' || (!!cfg.signal && sgMQ.matches)); // (Signal also needs the server to have it set up)
|
||||
const allowed = id => id === 'planner' ? (allowed('calendar') || allowed('tasks')) : id === 'links' ? (!ME || ME.tabs.includes('links') || ME.tabs.includes('verse')) : (!ME || ME.tabs.includes(id)) && (id !== 'signal' || (!!cfg.signal && sgMQ.matches)); // (Signal also needs the server to have it set up)
|
||||
/** Pull saved settings from the server into this browser; push up anything only this browser has (first run / migration). */
|
||||
async function loadSettings() {
|
||||
try {
|
||||
@@ -149,9 +149,9 @@ const fmtDate = s => new Date(s * 1000).toLocaleDateString(undefined, { month: '
|
||||
const TABS = [['home', 'Home'], ['files', 'Files'], ['music', 'Music'], ['mail', 'Mail'], ['signal', 'Signal'], ['tasks', 'Tasks'], ['calendar', 'Calendar'], ['search', 'Search'], ['links', 'Links'], ['office', 'Office'], ['audio', 'Audiobooks'], ['podcast', 'Podcast'], ['verse', 'Podcast Admin'], ['settings', 'Settings']];
|
||||
const TAB_ICON = { home: 'home', mail: 'mail', signal: 'chat', files: 'files', office: 'doc', audio: 'audio', verse: 'book', podcast: 'mic', music: 'music', settings: 'cog', calendar: 'cal', search: 'search', tasks: 'check', links: 'link', planner: 'cal' };
|
||||
/** The menu entries this person sees: Calendar and Tasks share one "Planner" entry (each part still needs its own permission). */
|
||||
const navTabs = () => { const out = []; let done = false; for (const t of TABS) { if (!allowed(t[0])) continue; if (t[0] === 'tasks' || t[0] === 'calendar') { if (!done) { out.push(['planner', 'Planner']); done = true; } continue; } out.push(t); } return out; };
|
||||
const navTabs = () => { const out = []; let done = false; for (const t of TABS) { if (t[0] === 'verse' || !allowed(t[0])) continue; if (t[0] === 'tasks' || t[0] === 'calendar') { if (!done) { out.push(['planner', 'Planner']); done = true; } continue; } out.push(t); } return out; };
|
||||
/** Menu entries grouped by kind. Anything not listed lands in the last group so a new tab is never lost. */
|
||||
const NAV_GROUPS = [['', ['home', 'search']], ['Work', ['planner', 'files', 'office', 'links']], ['Communicate', ['mail', 'signal']], ['Media', ['music', 'audio', 'podcast', 'verse']], ['', ['settings']]];
|
||||
const NAV_GROUPS = [['', ['home', 'search']], ['Work', ['planner', 'files', 'office', 'links']], ['Communicate', ['mail', 'signal']], ['Media', ['music', 'audio', 'podcast']], ['', ['settings']]];
|
||||
const navGroups = (tabs = navTabs()) => {
|
||||
const out = NAV_GROUPS.map(([label, ids]) => ({ label, tabs: ids.map(id => tabs.find(t => t[0] === id)).filter(Boolean) }));
|
||||
const known = new Set(NAV_GROUPS.flatMap(g => g[1])); out[out.length - 2].tabs.push(...tabs.filter(t => !known.has(t[0])));
|
||||
@@ -189,7 +189,7 @@ function openMore() {
|
||||
el.querySelectorAll('a.row').forEach(a => a.addEventListener('click', closeSheet));
|
||||
}
|
||||
|
||||
const routesOf = () => ({ planner: viewPlanner, home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: viewAudio, verse: viewVerse, podcast: viewPodcast, music: viewMusic, tasks: viewTasks, search: viewSearch, calendar: viewCalendar, signal: viewSignal, links: viewLinks, settings: viewSettings, edit: viewEdit });
|
||||
const routesOf = () => ({ planner: viewPlanner, home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: viewAudio, verse: () => location.replace('#/links'), podcast: viewPodcast, music: viewMusic, tasks: viewTasks, search: viewSearch, calendar: viewCalendar, signal: viewSignal, links: viewLinks, settings: viewSettings, edit: viewEdit });
|
||||
let editor;
|
||||
// Menu on a tablet or computer: collapse it to icons (wide screens), or hide it completely. Remembered on this device.
|
||||
const navPref = () => { try { return JSON.parse(localStorage.getItem('nav_pref')) || {}; } catch { return {}; } };
|
||||
@@ -255,9 +255,8 @@ function homeApps() {
|
||||
office: () => tile('#/office', '#e0742b', 'doc', 'Office', 'OnlyOffice'),
|
||||
audio: () => tile('#/audio', '#8a5cf0', 'audio', 'Audiobooks', 'Listen'),
|
||||
podcast: () => tile('#/podcast', '#c2417a', 'mic', 'Podcast', 'Podcasts'),
|
||||
verse: () => cfg.verse ? tile(cfg.verse, '#0f9d9a', 'book', 'Podcast Admin', 'Opens in a new tab', true) : '',
|
||||
signal: () => tile('#/signal', '#3a76f0', 'chat', 'Signal', sg.unread ? sg.unread + ' unread' : 'Messages'),
|
||||
links: () => tile('#/links', '#0f9d9a', 'link', 'Links', (store.get('links', []) || []).length + ' saved'),
|
||||
links: () => tile('#/links', '#0f9d9a', 'link', 'Links', allLinks().length + ' saved'),
|
||||
mail: () => tile('#/mail', '#4f7cff', 'mail', 'Mail', mailUnread ? mailUnread + ' unread' : 'Outlook.com'),
|
||||
};
|
||||
return navGroups().filter(g => g.tabs.some(t => T[t[0]])).map(g => `<h2>${esc(g.label || 'Apps')}</h2><div class="grid">${g.tabs.map(t => T[t[0]] ? T[t[0]]() : '').join('')}</div>`).join('');
|
||||
|
||||
+7
-4
@@ -4,22 +4,25 @@ const safeUrl = s => { let t = String(s || '').trim(); if (!t) return ''; if (!/
|
||||
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 = linkList(), q = linkQuery.trim().toLowerCase();
|
||||
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 = `<div class="wrap"><h1>Links</h1>
|
||||
<div class="bar"><input type="search" id="lq" placeholder="Filter links" value="${esc(linkQuery)}" aria-label="Filter links" style="flex:1"><button class="btn" id="ladd">${icon('plus')}Add</button></div>
|
||||
<div class="bar"><input type="search" id="lq" placeholder="Filter links" value="${esc(linkQuery)}" aria-label="Filter links" style="flex:1">${!ME || ME.tabs.includes('links') ? `<button class="btn" id="ladd">${icon('plus')}Add</button>` : ''}</div>
|
||||
${all.length ? '' : '<p class="empty">No links yet. Add the sites you use most; they are saved on your server and show up on every device.</p>'}
|
||||
${all.length && !shown.length ? '<p class="empty">Nothing matches.</p>' : ''}
|
||||
${groups.map(g => `${g ? `<h2>${esc(g)}</h2>` : (groups.length > 1 ? '<h2>Other</h2>' : '')}<div class="list">${shown.filter(l => (l.group || '') === g).map(l => `
|
||||
<div class="row"><a class="grow" href="${esc(safeUrl(l.url))}" target="_blank" rel="noopener noreferrer" style="display:flex;align-items:center;gap:12px;color:inherit;text-decoration:none;min-width:0">
|
||||
<span class="ic" style="background:${linkColor(l)};color:#fff;font-weight:700">${esc([...(l.title || l.url)][0].toUpperCase())}</span>
|
||||
<span class="grow" style="min-width:0"><b>${esc(l.title || l.url)}</b><small style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(new URL(safeUrl(l.url)).host)}</small></span></a>
|
||||
<button class="btn ghost" data-edit="${esc(l.id)}" aria-label="Edit ${esc(l.title)}" style="min-height:36px">Edit</button></div>`).join('')}</div>`).join('')}</div>`;
|
||||
${l.builtin ? '<small style="color:var(--muted)">Built in</small>' : `<button class="btn ghost" data-edit="${esc(l.id)}" aria-label="Edit ${esc(l.title)}" style="min-height:36px">Edit</button>`}</div>`).join('')}</div>`).join('')}</div>`;
|
||||
$('#lq').oninput = e => { linkQuery = e.target.value; const p = e.target.selectionStart; viewLinks(); const n = $('#lq'); n.focus(); n.setSelectionRange(p, p); };
|
||||
$('#ladd').onclick = () => linkSheet();
|
||||
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) {
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ function openPalette() {
|
||||
const build = () => {
|
||||
const q = input.value.trim(), ql = q.toLowerCase();
|
||||
let all = base();
|
||||
if (typeof linkList === 'function' && allowed('links')) all = all.concat(linkList().map(l => ({ label: l.title || l.url, hint: 'Link · ' + new URL(l.url).host, ic: 'link', run: () => window.open(l.url, '_blank', 'noopener'), kw: (l.group || '') + ' ' + l.url })));
|
||||
if (typeof allLinks === 'function' && allowed('links')) all = all.concat(allLinks().map(l => ({ label: l.title || l.url, hint: 'Link · ' + new URL(l.url).host, ic: 'link', run: () => window.open(l.url, '_blank', 'noopener'), kw: (l.group || '') + ' ' + l.url })));
|
||||
let res = ql ? all.filter(i => (i.label + ' ' + i.kw).toLowerCase().includes(ql)) : all.slice(0, 12);
|
||||
if (ql) {
|
||||
res = res.sort((a, b) => (b.label.toLowerCase().startsWith(ql)) - (a.label.toLowerCase().startsWith(ql)));
|
||||
|
||||
@@ -98,7 +98,7 @@ function viewSearch() {
|
||||
music: !plexReady ? undefined : music.status === 'fulfilled' ? { n: music.value.length } : { err: music.reason && music.reason.message },
|
||||
});
|
||||
const notes = [...(h.errors || [])].map(e => `<p class="sub">${esc(e)}</p>`).join('');
|
||||
const linkHtml = allowed('links') ? linkList().filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(lower)).slice(0, 8).map(l => `<a class="row" href="${esc(safeUrl(l.url))}" target="_blank" rel="noopener noreferrer"><span class="ic" style="background:${linkColor(l)}">${icon('link')}</span><span class="grow"><b>${esc(l.title || l.url)}</b><small>${esc(new URL(safeUrl(l.url)).host)}${l.group ? ' · ' + esc(l.group) : ''}</small></span></a>`).join('') : '';
|
||||
const linkHtml = allowed('links') ? allLinks().filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(lower)).slice(0, 8).map(l => `<a class="row" href="${esc(safeUrl(l.url))}" target="_blank" rel="noopener noreferrer"><span class="ic" style="background:${linkColor(l)}">${icon('link')}</span><span class="grow"><b>${esc(l.title || l.url)}</b><small>${esc(new URL(safeUrl(l.url)).host)}${l.group ? ' · ' + esc(l.group) : ''}</small></span></a>`).join('') : '';
|
||||
res.innerHTML = card + sec('Tasks', taskHtml) + sec('Links', linkHtml) + sec('Events', eventHtml) + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + notes;
|
||||
if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !card) res.innerHTML = '<p class="empty">No results. The line above shows what was searched.</p>' + notes;
|
||||
res.querySelectorAll('button[data-f]').forEach(b => b.onclick = () => { const [r, p, n, dd] = JSON.parse(b.dataset.f); openFile(r, p, n, dd); });
|
||||
|
||||
Reference in new issue
Block a user