diff --git a/.env.example b/.env.example
index 1f99615..23b0654 100644
--- a/.env.example
+++ b/.env.example
@@ -9,3 +9,4 @@ COOKIE_SECRET=
AUDIOBOOKS_URL=https://audiobooks.necloud.us
# First-time passkey enrolment. If unset, a random code is printed in the server log while no passkey exists.
HUB_SETUP_CODE=
+VERSE_URL=https://versebyversewithnate.us/admin
diff --git a/docker-compose.yml b/docker-compose.yml
index 1e2facd..e8fa5f7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,7 @@ services:
SEAFILE_URL: ${SEAFILE_URL:-https://seafile.necloud.us}
ONLYOFFICE_URL: ${ONLYOFFICE_URL:-https://onlyoffice.necloud.us}
AUDIOBOOKS_URL: ${AUDIOBOOKS_URL:-https://audiobooks.necloud.us}
+ VERSE_URL: ${VERSE_URL:-https://versebyversewithnate.us/admin}
ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-} # same value as the OnlyOffice server's JWT secret
PUBLIC_URL: ${PUBLIC_URL:-} # e.g. https://hub.necloud.us (OnlyOffice posts saved edits here)
HUB_SETUP_CODE: ${HUB_SETUP_CODE:-} # first passkey; if empty, a code is printed in the log
diff --git a/public/app.js b/public/app.js
index dafeae5..dbc17e0 100644
--- a/public/app.js
+++ b/public/app.js
@@ -38,6 +38,7 @@ const ICONS = {
more: ' ',
cal: ' ',
search: ' ',
+ book: ' ',
trash: ' ',
};
const icon = (n, cls = '') => `${ICONS[n]} `;
@@ -61,7 +62,7 @@ function mailUrl(m, kind) {
return base; // iCloud has no sign-in hint
}
-let cfg = { audiobooks: 'https://audiobooks.necloud.us', seafile: 'https://seafile.necloud.us', onlyoffice: 'https://onlyoffice.necloud.us' };
+let cfg = { verse: 'https://versebyversewithnate.us/admin', audiobooks: 'https://audiobooks.necloud.us', seafile: 'https://seafile.necloud.us', onlyoffice: 'https://onlyoffice.necloud.us' };
// ---- Seafile client (via same-origin proxy)
const sf = {
@@ -95,8 +96,8 @@ const fmtSize = b => b < 1024 ? b + ' B' : b < 1048576 ? (b / 1024).toFixed(0) +
const fmtDate = s => new Date(s * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
// ---- Navigation
-const TABS = [['home', 'Home'], ['files', 'Files'], ['music', 'Music'], ['mail', 'Mail'], ['calendar', 'Calendar'], ['search', 'Search'], ['office', 'Office'], ['audio', 'Audio'], ['settings', 'Settings']];
-const TAB_ICON = { home: 'home', mail: 'mail', files: 'files', office: 'doc', audio: 'audio', music: 'music', settings: 'cog', calendar: 'cal', search: 'search' };
+const TABS = [['home', 'Home'], ['files', 'Files'], ['music', 'Music'], ['mail', 'Mail'], ['calendar', 'Calendar'], ['search', 'Search'], ['office', 'Office'], ['audio', 'Audio'], ['verse', 'Verse admin'], ['settings', 'Settings']];
+const TAB_ICON = { home: 'home', mail: 'mail', files: 'files', office: 'doc', audio: 'audio', verse: 'book', music: 'music', settings: 'cog', calendar: 'cal', search: 'search' };
const PRIMARY = ['home', 'files', 'music', 'mail']; // phone tab bar; the rest sit behind "More"
const phone = matchMedia('(max-width: 767px)');
let activeTab = 'home';
@@ -120,7 +121,7 @@ function openMore() {
el.querySelectorAll('a.row').forEach(a => a.addEventListener('click', closeSheet));
}
-const routes = { home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: viewAudio, music: viewMusic, search: viewSearch, calendar: viewCalendar, settings: viewSettings, edit: viewEdit };
+const routes = { home: viewHome, mail: viewMail, files: viewFiles, office: viewOffice, audio: viewAudio, verse: viewVerse, music: viewMusic, search: viewSearch, calendar: viewCalendar, settings: viewSettings, edit: viewEdit };
let editor;
function route() {
const [, id = 'home', ...rest] = location.hash.split('/');
@@ -157,6 +158,7 @@ function viewHome() {
${tile('#/calendar', '#d9453a', 'cal', 'Calendar', 'Agenda')}
${tile('#/office', '#e0742b', 'doc', 'Office', 'OnlyOffice')}
${tile('#/audio', '#8a5cf0', 'audio', 'Audio', 'Audiobooks')}
+ ${tile(cfg.verse, '#0f9d9a', 'book', 'Verse admin', 'Opens in a new tab', true)}
${tile('#/mail', '#4f7cff', 'mail', 'Mail', '4 accounts')}
Mail
@@ -507,6 +509,14 @@ async function newOfficeDoc(x, label) {
// ---- Audiobooks: embedded site. Sign-in details saved in Settings (this browser only) can be copied with one tap,
// because a page can't type into another site's login form.
const abCreds = () => store.get('ab_creds', { u: '', p: '' });
+// Verse by Verse admin: the site refuses to be framed (X-Frame-Options), so this is a launcher.
+function viewVerse() {
+ const url = cfg.verse, host = new URL(url).host;
+ $('#view').innerHTML = `
Verse admin ${esc(host + new URL(url).pathname)}
+
This site doesn't allow being shown inside another app, so it opens in its own tab.
+
`;
+}
+
function viewAudio() {
const url = cfg.audiobooks, c = abCreds();
$('#view').innerHTML = `
Audio
diff --git a/server.js b/server.js
index 277a632..a24911c 100644
--- a/server.js
+++ b/server.js
@@ -11,6 +11,7 @@ const ONLYOFFICE_URL = (process.env.ONLYOFFICE_URL || 'https://onlyoffice.neclou
const AUDIOBOOKS_URL = (process.env.AUDIOBOOKS_URL || 'https://audiobooks.necloud.us').replace(/\/$/, '');
const auth = require('./auth');
const cal = require('./cal');
+const VERSE_URL = process.env.VERSE_URL || 'https://versebyversewithnate.us/admin';
const PUBLIC = path.join(__dirname, 'public');
const TYPES = {
@@ -282,7 +283,7 @@ http.createServer((req, res) => {
}
if (req.url === '/api/config') {
res.writeHead(200, { 'content-type': 'application/json' });
- return res.end(JSON.stringify({ seafile: SEAFILE_URL, onlyoffice: ONLYOFFICE_URL, audiobooks: AUDIOBOOKS_URL, editor: true, jwt: !!JWT_SECRET }));
+ return res.end(JSON.stringify({ seafile: SEAFILE_URL, onlyoffice: ONLYOFFICE_URL, audiobooks: AUDIOBOOKS_URL, verse: VERSE_URL, editor: true, jwt: !!JWT_SECRET }));
}
if (req.method === 'POST' && req.url === '/api/oo/config') return ooConfig(req, res);
if (req.method === 'POST' && req.url.startsWith('/api/oo/callback')) return ooCallback(req, res);