Bundled Roundcube webmail in the Mail tab, optional Microsoft 365 OAuth sign-in, collapsible menu
build-container / build (push) Successful in 2m42s
build-container / build (push) Successful in 2m42s
- docker-compose: roundcube service with inline config (Gmail/iCloud/Yahoo presets, framing allowed, persistent key); only reachable through the hub - hub passes /webmail/ through to Roundcube behind the passkey sign-in; Microsoft's sign-in page opens in the whole window and returns to the Mail tab - M365_* settings add 'Login with Microsoft 365' (README has the Entra app registration steps) - Side menu collapses to icons or hides completely (remembered per device, Ctrl/Cmd+B) - tools/webmail-test.js Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
318006dba3
commit
cdf862427e
8 files changed
+230
-11
No files matched your search
+13
-2
@@ -49,6 +49,7 @@ async function loadSettings() {
|
||||
}
|
||||
|
||||
const ICONS = {
|
||||
menu: '<path d="M4 7h16M4 12h16M4 17h16"/>',
|
||||
home: '<path d="M3 11l9-8 9 8M5 10v10h14V10"/>',
|
||||
mail: '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>',
|
||||
files: '<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>',
|
||||
@@ -157,7 +158,10 @@ function renderNav(active = activeTab) {
|
||||
$('#nav').innerHTML = mine.filter(t => PRIMARY.includes(t[0])).map(tab).join('') +
|
||||
(rest.length ? `<a href="#" id="more" class="${more ? 'on' : ''}" role="button">${icon('more')}<span>More</span></a>` : '');
|
||||
const mo = $('#more'); if (mo) mo.onclick = e => { e.preventDefault(); openMore(); };
|
||||
} else $('#nav').innerHTML = brand + mine.map(tab).join('');
|
||||
} else {
|
||||
$('#nav').innerHTML = brand + mine.map(tab).join('') + `<div class="spacer"></div><button id="navcol" class="navcol" aria-label="Collapse the menu" title="Collapse or expand the menu">${icon('back')}<span>Collapse</span></button>`;
|
||||
$('#navcol').onclick = () => setNav({ mini: !navPref().mini });
|
||||
}
|
||||
}
|
||||
phone.addEventListener('change', () => renderNav());
|
||||
|
||||
@@ -169,6 +173,12 @@ function openMore() {
|
||||
|
||||
const routes = { home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: viewAudio, verse: viewVerse, podcast: viewPodcast, music: viewMusic, tasks: viewTasks, search: viewSearch, calendar: viewCalendar, 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 {}; } };
|
||||
function applyNav() { const p = navPref(); document.body.classList.toggle('nav-mini', !!p.mini); document.body.classList.toggle('nav-hidden', !!p.hidden); }
|
||||
function setNav(patch) { try { localStorage.setItem('nav_pref', JSON.stringify({ ...navPref(), ...patch })); } catch {} applyNav(); }
|
||||
applyNav();
|
||||
addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'b' && !/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName || '')) { e.preventDefault(); setNav({ hidden: !navPref().hidden }); } });
|
||||
// In-app Back / Forward (an installed app has no browser buttons). Each history entry remembers its position so we know what is possible.
|
||||
let navIdx = 0, navMax = 0, navStarted = false;
|
||||
const parentHash = () => { const p = location.hash.split('/'); return p.length > 3 ? p.slice(0, -1).join('/') : ''; }; // e.g. #/files/lib/a/b -> #/files/lib/a
|
||||
@@ -183,7 +193,8 @@ function drawNav() {
|
||||
const canBack = navIdx > 0 || !!parentHash(), canFwd = navIdx < navMax;
|
||||
if (!el.dataset.on) {
|
||||
el.dataset.on = 1;
|
||||
el.innerHTML = `<button class="ibtn" id="nvb" aria-label="Back" title="Back">${icon('back')}</button><button class="ibtn" id="nvf" aria-label="Forward" title="Forward">${icon('chev')}</button><span id="nvt"></span>`;
|
||||
el.innerHTML = `<button class="ibtn navtoggle" id="nvm" aria-label="Show or hide the menu" title="Show or hide the menu (Ctrl/Cmd+B)">${icon('menu')}</button><button class="ibtn" id="nvb" aria-label="Back" title="Back">${icon('back')}</button><button class="ibtn" id="nvf" aria-label="Forward" title="Forward">${icon('chev')}</button><span id="nvt"></span>`;
|
||||
$('#nvm').onclick = () => setNav({ hidden: !navPref().hidden });
|
||||
$('#nvb').onclick = () => { if (navIdx > 0) history.back(); else if (parentHash()) location.hash = parentHash(); };
|
||||
$('#nvf').onclick = () => history.forward();
|
||||
}
|
||||
|
||||
@@ -318,3 +318,28 @@ body:not(.editing) #view { padding-top: 8px; }
|
||||
.mact .btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 46px; }
|
||||
.mact .btn svg { width: 20px; height: 20px; }
|
||||
.mact .btn:not(.ghost) { background: var(--mc); color: #fff; }
|
||||
|
||||
/* Collapsible menu (tablet / computer) */
|
||||
.navtoggle { display: none; }
|
||||
.navcol { display: none; }
|
||||
@media (min-width: 768px) {
|
||||
#nav { overflow-y: auto; scrollbar-width: none; }
|
||||
.navtoggle { display: grid; }
|
||||
body.nav-hidden #nav { display: none; }
|
||||
body.nav-hidden #navbar { padding-left: 12px; }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.navcol { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 14px; border: 0; background: none; border-radius: 12px; color: var(--muted); font-size: 14px; }
|
||||
.navcol:hover { background: var(--panel2); color: var(--text); }
|
||||
.navcol svg { width: 20px; height: 20px; }
|
||||
body.nav-mini #nav { width: 76px; padding: calc(var(--sat) + 12px) 0 12px var(--sal); }
|
||||
body.nav-mini #nav a { flex-direction: column; justify-content: center; gap: 2px; padding: 0; font-size: 10px; min-height: 50px; border-radius: 0; flex: none; }
|
||||
body.nav-mini #nav a svg { width: 22px; height: 22px; }
|
||||
body.nav-mini #nav a.on { background: none; }
|
||||
body.nav-mini .brand { display: block; width: 36px; height: 36px; margin: 4px auto 12px; }
|
||||
body.nav-mini .brand img { width: 36px; height: 36px; border-radius: 10px; }
|
||||
body.nav-mini .brand span { display: none; }
|
||||
body.nav-mini .navcol { flex-direction: column; justify-content: center; gap: 2px; padding: 0; min-height: 56px; font-size: 11px; }
|
||||
body.nav-mini .navcol svg { transform: rotate(180deg); }
|
||||
body.nav-mini .navcol span { display: none; }
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@ self.addEventListener('activate', e => {
|
||||
});
|
||||
self.addEventListener('fetch', e => {
|
||||
const u = new URL(e.request.url);
|
||||
if (e.request.method !== 'GET' || u.origin !== location.origin || u.pathname.startsWith('/sf/') || u.pathname.startsWith('/plex/') || u.pathname.startsWith('/api/')) return;
|
||||
if (e.request.method !== 'GET' || u.origin !== location.origin || u.pathname.startsWith('/sf/') || u.pathname.startsWith('/plex/') || u.pathname.startsWith('/webmail') || u.pathname.startsWith('/api/')) return;
|
||||
e.respondWith(fetch(e.request).then(r => {
|
||||
if (r.ok) { const copy = r.clone(); caches.open(CACHE).then(c => c.put(e.request, copy)); }
|
||||
return r;
|
||||
|
||||
Reference in new issue
Block a user