Theseus: bundle Pithos 0.3.9
This commit is contained in:
parent
6fd44db1d6
commit
556a22b062
3 changed files with 75 additions and 14 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "pithos",
|
"id": "pithos",
|
||||||
"name": "Pithos",
|
"name": "Pithos",
|
||||||
"version": "0.3.8",
|
"version": "0.3.9",
|
||||||
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ pre.snippet { background: var(--surface-2); padding: 10px 12px; border-radius: 8
|
||||||
.steps span.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
.steps span.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
||||||
|
|
||||||
/* modal + toasts */
|
/* modal + toasts */
|
||||||
.modal-backdrop { position: fixed; inset: 0; background: rgb(0 0 0 / .45); display: grid; place-items: center; padding: 16px; z-index: 10; }
|
.modal-backdrop { position: fixed; inset: 0; background: rgb(0 0 0 / .45); display: grid; place-items: center; padding: 16px; z-index: 25; }
|
||||||
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 22px; width: min(560px, 100%); max-height: 90vh; overflow: auto; }
|
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 22px; width: min(560px, 100%); max-height: 90vh; overflow: auto; }
|
||||||
.modal h2 { margin-bottom: 14px; }
|
.modal h2 { margin-bottom: 14px; }
|
||||||
.modal .foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
.modal .foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
||||||
|
|
@ -399,3 +399,20 @@ body.has-foot.has-player .toasts { bottom: 112px; }
|
||||||
.narrow-only { display: inline-flex; align-items: center; }
|
.narrow-only { display: inline-flex; align-items: center; }
|
||||||
table.files td.actions { padding-left: 2px; }
|
table.files td.actions { padding-left: 2px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Player volume */
|
||||||
|
.pl-volume { position: relative; display: flex; align-items: center; gap: 4px; flex: none; }
|
||||||
|
.pl-volpop { display: flex; align-items: center; }
|
||||||
|
.pl-volpop > .pl-btn { display: none; }
|
||||||
|
.pl-vol { width: 90px; accent-color: var(--accent); margin: 0; }
|
||||||
|
.pl-volbtn { font-size: 15px; }
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.pl-volpop { display: none; position: absolute; bottom: calc(100% + 10px); right: -6px; padding: 8px 10px; gap: 6px;
|
||||||
|
background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 24px rgb(0 0 0 / .2); }
|
||||||
|
.pl-volume.open .pl-volpop { display: flex; }
|
||||||
|
.pl-volpop > .pl-btn { display: grid; }
|
||||||
|
.pl-vol { width: 120px; }
|
||||||
|
}
|
||||||
|
/* "More options" disclosure in dialogs */
|
||||||
|
details.more-opts > summary { cursor: pointer; color: var(--muted); font-size: 13px; }
|
||||||
|
details.more-opts > summary:hover { color: var(--text); }
|
||||||
|
|
|
||||||
|
|
@ -1291,7 +1291,8 @@ async function accessDialog(user, bucket) {
|
||||||
opts.map(([val, label, desc]) => h('label', { class: 'check', style: 'align-items:flex-start' },
|
opts.map(([val, label, desc]) => h('label', { class: 'check', style: 'align-items:flex-start' },
|
||||||
h('input', { type: 'radio', name: 'acc', checked: choice === val, onchange: () => { choice = val; } }),
|
h('input', { type: 'radio', name: 'acc', checked: choice === val, onchange: () => { choice = val; } }),
|
||||||
h('span', {}, h('strong', {}, label), h('br'), h('span', { class: 'small muted' }, desc)))),
|
h('span', {}, h('strong', {}, label), h('br'), h('span', { class: 'small muted' }, desc)))),
|
||||||
h('p', { class: 'small muted', style: 'margin:0' }, 'Public objects are served at ', h('code', {}, `${endpoint}/${bucket}/<key>`), '. Writes and deletes always need a key.'),
|
h('p', { class: 'small muted', style: 'margin:0' }, 'Public objects are served at ', h('code', {}, `${endpoint}/${bucket}/<key>`), '. Writes and deletes always need a key. You can change this at any time.'),
|
||||||
|
localOnly() && h('div', { class: 'banner info small', style: 'margin:0' }, h('span', {}, 'Right now s3d listens only on this computer, so "public" means public to programs on this computer. Other people reach public files once s3d has a public address.')),
|
||||||
h('p', { class: 'small muted', style: 'margin:0' }, 'Access applies to the whole bucket: s3d cannot make a single folder public, or give other people write rights. Keep public files in a bucket of their own, or send a share link for a single file.')),
|
h('p', { class: 'small muted', style: 'margin:0' }, 'Access applies to the whole bucket: s3d cannot make a single folder public, or give other people write rights. Keep public files in a bucket of their own, or send a share link for a single file.')),
|
||||||
[{ label: 'Cancel', result: null }, { label: 'Save', kind: 'primary', submit: true, value: () => choice }]);
|
[{ label: 'Cancel', result: null }, { label: 'Save', kind: 'primary', submit: true, value: () => choice }]);
|
||||||
if (!v || v === current) return;
|
if (!v || v === current) return;
|
||||||
|
|
@ -1526,26 +1527,40 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
||||||
try { const r = await api('DELETE', `${base}/prefix?prefix=${encodeURIComponent(p)}`); toast(`Deleted ${r.deleted} objects`); load(); } catch (e) { fail(e); }
|
try { const r = await api('DELETE', `${base}/prefix?prefix=${encodeURIComponent(p)}`); toast(`Deleted ${r.deleted} objects`); load(); } catch (e) { fail(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Share: the link is made as soon as the dialog opens and remade when the
|
||||||
|
// validity or address changes. s3d listens only on this computer, so
|
||||||
|
// without an address of your own the link opens only here; it says so.
|
||||||
async function share(key) {
|
async function share(key) {
|
||||||
const expires = h('select', {}, [['3600', '1 hour'], ['86400', '1 day'], ['604800', '7 days (max)']].map(([v, l]) => h('option', { value: v }, l)));
|
const expires = h('select', {}, [['3600', '1 hour'], ['86400', '1 day'], ['604800', '7 days (max)']].map(([v, l]) => h('option', { value: v, selected: v === '86400' }, l)));
|
||||||
const savedEndpoint = localStorage.getItem('pithos.publicEndpoint') || '';
|
const savedEndpoint = localStorage.getItem('pithos.publicEndpoint') || '';
|
||||||
const endpoint = h('input', { type: 'url', value: savedEndpoint, placeholder: `http://${state.status?.config.apiAddress}` });
|
const endpoint = h('input', { type: 'url', value: savedEndpoint, placeholder: 'https://files.example.com' });
|
||||||
const out = h('div');
|
const out = h('div', {}, h('p', { class: 'muted small' }, 'Making the link…'));
|
||||||
|
const note = h('div');
|
||||||
|
let link = '';
|
||||||
const gen = async () => {
|
const gen = async () => {
|
||||||
const q = new URLSearchParams({ key, expires: expires.value });
|
const q = new URLSearchParams({ key, expires: expires.value });
|
||||||
if (endpoint.value.trim()) { q.set('endpoint', endpoint.value.trim()); localStorage.setItem('pithos.publicEndpoint', endpoint.value.trim()); }
|
const own = endpoint.value.trim();
|
||||||
|
try { localStorage.setItem('pithos.publicEndpoint', own); } catch {}
|
||||||
|
if (own) q.set('endpoint', own);
|
||||||
try {
|
try {
|
||||||
const { url } = await api('GET', `${base}/presign?${q}`);
|
({ url: link } = await api('GET', `${base}/presign?${q}`));
|
||||||
put(out, secretRow('Link', url));
|
put(out, secretRow('Link', link));
|
||||||
} catch (e) { fail(e); }
|
} catch (e) { put(out, h('p', { class: 'error small' }, e.message)); }
|
||||||
return false;
|
put(note, !own && localOnly() && h('div', { class: 'banner info small', style: 'margin:0' },
|
||||||
|
h('span', {}, 'This link opens only on this computer: s3d listens on ', h('code', {}, state.status?.config.apiAddress || '127.0.0.1'), ', which other devices cannot reach. To share with other people, s3d needs a public address (for example your own server in front of it).')));
|
||||||
};
|
};
|
||||||
|
expires.addEventListener('change', gen);
|
||||||
|
endpoint.addEventListener('change', gen);
|
||||||
|
gen();
|
||||||
await modal('Share link', h('div', { class: 'stack' },
|
await modal('Share link', h('div', { class: 'stack' },
|
||||||
h('p', { class: 'small muted', style: 'margin:0' }, 'A signed link that works without a key until it expires.'),
|
h('p', { class: 'small muted', style: 'margin:0' }, 'A signed link that works without a key until it expires.'),
|
||||||
h('label', { class: 'field' }, h('span', {}, 'Valid for'), expires),
|
h('label', { class: 'field' }, h('span', {}, 'Valid for'), expires),
|
||||||
h('label', { class: 'field' }, h('span', {}, 'Public address of s3d'), endpoint, h('small', {}, 'The address recipients will use to reach s3d (e.g. behind a reverse proxy). Leave blank for the local endpoint.')),
|
out,
|
||||||
out),
|
note,
|
||||||
[{ label: 'Close', result: true }, { label: 'Create link', kind: 'primary', submit: true, value: gen }]);
|
h('details', { class: 'more-opts', open: !!savedEndpoint },
|
||||||
|
h('summary', {}, 'Use my own public address'),
|
||||||
|
h('label', { class: 'field', style: 'margin-top:8px' }, endpoint, h('small', {}, 'The address where other people reach this s3d, e.g. a reverse proxy on your server. The link is remade when you change it.')))),
|
||||||
|
[{ label: 'Copy link', value: async () => { if (link) await copy(link, 'Link copied'); return false; } }, { label: 'Done', kind: 'primary', submit: true, result: true }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
@ -1577,9 +1592,33 @@ function player() {
|
||||||
const prevBtn = h('button', { type: 'button', class: 'pl-btn', 'aria-label': 'Previous', title: 'Previous', onclick: () => prev() }, '⏮');
|
const prevBtn = h('button', { type: 'button', class: 'pl-btn', 'aria-label': 'Previous', title: 'Previous', onclick: () => prev() }, '⏮');
|
||||||
const nextBtn = h('button', { type: 'button', class: 'pl-btn', 'aria-label': 'Next', title: 'Next', onclick: () => next() }, '⏭');
|
const nextBtn = h('button', { type: 'button', class: 'pl-btn', 'aria-label': 'Next', title: 'Next', onclick: () => next() }, '⏭');
|
||||||
const closeBtn = h('button', { type: 'button', class: 'pl-btn', 'aria-label': 'Stop and close the player', title: 'Close', onclick: () => close() }, '✕');
|
const closeBtn = h('button', { type: 'button', class: 'pl-btn', 'aria-label': 'Stop and close the player', title: 'Close', onclick: () => close() }, '✕');
|
||||||
|
// Volume: a slider beside a mute button; on narrow screens the button
|
||||||
|
// opens the slider in a small pop-up above it. Remembered between visits.
|
||||||
|
const saved = (() => { try { return JSON.parse(localStorage.getItem('pithos.volume') || 'null'); } catch { return null; } })();
|
||||||
|
audio.volume = saved && isFinite(saved.v) ? Math.min(1, Math.max(0, saved.v)) : 1;
|
||||||
|
audio.muted = !!saved?.m;
|
||||||
|
const volIcon = () => (audio.muted || audio.volume === 0 ? '🔇' : audio.volume < 0.5 ? '🔉' : '🔊');
|
||||||
|
const vol = h('input', { type: 'range', min: '0', max: '1', step: '0.05', value: String(audio.volume), class: 'pl-vol', 'aria-label': 'Volume',
|
||||||
|
oninput: () => { audio.volume = +vol.value; if (audio.volume > 0) audio.muted = false; } });
|
||||||
|
const volBtn = h('button', { type: 'button', class: 'pl-btn pl-volbtn', title: 'Mute', 'aria-label': 'Mute', onclick: () => {
|
||||||
|
if (matchMedia('(max-width: 760px)').matches) { volBox.classList.toggle('open'); return; }
|
||||||
|
audio.muted = !audio.muted;
|
||||||
|
} }, volIcon());
|
||||||
|
const muteInPop = h('button', { type: 'button', class: 'pl-btn', title: 'Mute', 'aria-label': 'Mute', onclick: () => { audio.muted = !audio.muted; } }, '🔇');
|
||||||
|
const volBox = h('div', { class: 'pl-volume' }, volBtn, h('div', { class: 'pl-volpop' }, muteInPop, vol));
|
||||||
|
audio.addEventListener('volumechange', () => {
|
||||||
|
volBtn.textContent = volIcon();
|
||||||
|
const m = audio.muted ? 'Unmute' : 'Mute';
|
||||||
|
volBtn.title = muteInPop.title = m; muteInPop.setAttribute('aria-label', m);
|
||||||
|
muteInPop.textContent = audio.muted ? '🔈' : '🔇';
|
||||||
|
vol.value = String(audio.muted ? 0 : audio.volume);
|
||||||
|
try { localStorage.setItem('pithos.volume', JSON.stringify({ v: audio.volume, m: audio.muted })); } catch {}
|
||||||
|
});
|
||||||
|
document.addEventListener('mousedown', (e) => { if (!volBox.contains(e.target)) volBox.classList.remove('open'); });
|
||||||
const el = h('div', { class: 'player', role: 'region', 'aria-label': 'Music player', hidden: true },
|
const el = h('div', { class: 'player', role: 'region', 'aria-label': 'Music player', hidden: true },
|
||||||
h('div', { class: 'pl-controls' }, prevBtn, playBtn, nextBtn),
|
h('div', { class: 'pl-controls' }, prevBtn, playBtn, nextBtn),
|
||||||
h('div', { class: 'pl-info' }, h('div', { class: 'pl-head' }, title, where), h('div', { class: 'pl-seek' }, cur, seek, dur)),
|
h('div', { class: 'pl-info' }, h('div', { class: 'pl-head' }, title, where), h('div', { class: 'pl-seek' }, cur, seek, dur)),
|
||||||
|
volBox,
|
||||||
closeBtn);
|
closeBtn);
|
||||||
document.body.append(el);
|
document.body.append(el);
|
||||||
|
|
||||||
|
|
@ -1801,6 +1840,11 @@ $('#unlock-btn').addEventListener('click', async () => {
|
||||||
// hours; a newer release replaces the version with "↑ vX". One click
|
// hours; a newer release replaces the version with "↑ vX". One click
|
||||||
// downloads and verifies it, the next restarts Theseus to apply it.
|
// downloads and verifies it, the next restarts Theseus to apply it.
|
||||||
const UPDATES_URL = 'https://navigate.st/bns/theseus.x/extensions/pithos/updates.json';
|
const UPDATES_URL = 'https://navigate.st/bns/theseus.x/extensions/pithos/updates.json';
|
||||||
|
// s3d's S3 port is reachable only from this computer (the default).
|
||||||
|
function localOnly() {
|
||||||
|
const a = state.status?.config?.apiAddress || '';
|
||||||
|
return /^(127\.|localhost|\[::1\])/.test(a);
|
||||||
|
}
|
||||||
function cmpSemver(a, b) {
|
function cmpSemver(a, b) {
|
||||||
const pa = String(a || '0').split('.').map((n) => Number(n) || 0);
|
const pa = String(a || '0').split('.').map((n) => Number(n) || 0);
|
||||||
const pb = String(b || '0').split('.').map((n) => Number(n) || 0);
|
const pb = String(b || '0').split('.').map((n) => Number(n) || 0);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue