Office tab: create documents, spreadsheets and presentations
build-container / build (push) Successful in 2m49s
build-container / build (push) Successful in 2m49s
The Office tab was just the OnlyOffice welcome page. It now offers New document/spreadsheet/presentation, creates the file in Seafile from a valid blank template (empty 0-byte files often will not open in the editor) and opens it in the editor. Files > New uses the same templates. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd48ccf8e0
commit
b5603d2488
4 files changed
+67
-10
No files matched your search
+67
-10
@@ -319,15 +319,69 @@ function viewMail() {
|
||||
}
|
||||
|
||||
function viewOffice() {
|
||||
const url = cfg.onlyoffice;
|
||||
$('#view').innerHTML = `<div class="wrap"><h1>Office</h1>
|
||||
<p class="sub">OnlyOffice at ${esc(new URL(url).host)}. To edit a file from Seafile, open it in Files.</p>
|
||||
<div class="bar"><a class="btn" href="${esc(url)}" target="_blank" rel="noopener">Open in new tab</a>
|
||||
<a class="btn ghost" href="#/files">Go to Files</a></div>
|
||||
<iframe class="frame" src="${esc(url)}" title="OnlyOffice" allow="clipboard-read; clipboard-write; fullscreen"></iframe>
|
||||
<p class="sub" style="margin-top:8px">If the frame above is blank, your OnlyOffice server blocks embedding. Use the button, or allow this app's origin in its frame-ancestors / X-Frame-Options.</p></div>`;
|
||||
const docs = store.get('recents', []).filter(r => EDIT_EXT.has(ext(r.name)) && ext(r.name) !== 'txt').slice(0, 8);
|
||||
$('#view').innerHTML = `<div class="wrap"><h1>Office</h1><p class="sub">Create a file in Seafile and edit it right here.</p>
|
||||
<div class="grid">
|
||||
<button class="tile" data-x="docx" data-l="document"><span class="ic" style="background:#3d6bf0">${icon('doc')}</span><b>Document</b><small>Word (.docx)</small></button>
|
||||
<button class="tile" data-x="xlsx" data-l="spreadsheet"><span class="ic" style="background:#2f9e6e">${icon('doc')}</span><b>Spreadsheet</b><small>Excel (.xlsx)</small></button>
|
||||
<button class="tile" data-x="pptx" data-l="presentation"><span class="ic" style="background:#e0742b">${icon('doc')}</span><b>Presentation</b><small>PowerPoint (.pptx)</small></button>
|
||||
</div>
|
||||
<p class="sub" id="olib" style="margin-top:12px"></p>
|
||||
${docs.length ? `<h2>Recent documents</h2><div class="list">${docs.map((r, i) => `<button class="row" data-r="${i}"><span class="ic" style="background:#e0742b">${icon('file')}</span><span class="grow"><b>${esc(r.name)}</b><small>${esc(r.path.slice(0, r.path.lastIndexOf('/')) || '/')}</small></span></button>`).join('')}</div>` : ''}
|
||||
<div class="bar"><a class="btn ghost" href="#/files">Browse all files</a></div>
|
||||
<p class="sub">Editing server: <a href="${esc(cfg.onlyoffice)}" target="_blank" rel="noopener" style="color:var(--accent)">${esc(new URL(cfg.onlyoffice).host)}</a></p></div>`;
|
||||
$('#view').querySelectorAll('.tile[data-x]').forEach(t => t.onclick = () => newOfficeDoc(t.dataset.x, t.dataset.l));
|
||||
$('#view').querySelectorAll('button[data-r]').forEach(b => b.onclick = () => { const r = docs[b.dataset.r]; openFile(r.repo, r.path, r.name, r.path.slice(0, r.path.lastIndexOf('/')) || '/'); });
|
||||
if (sf.token()) sf.req('/api2/repos/').then(repos => {
|
||||
const r = repos.find(x => x.id === store.get('office_repo')) || (repos.length === 1 ? repos[0] : null);
|
||||
const el = $('#olib'); if (!el) return;
|
||||
el.innerHTML = r ? `New files go in <b>${esc(r.name)}</b> · <a href="#" id="chl" style="color:var(--accent)">change</a>` : 'You will be asked which library to use.';
|
||||
const c = $('#chl'); if (c) c.onclick = e => { e.preventDefault(); store.del('office_repo'); pickLibrary().then(() => viewOffice()).catch(() => {}); };
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Blank Office files come from valid templates (an empty 0-byte .docx/.xlsx/.pptx often will not open in the editor).
|
||||
async function createFromTemplate(repo, dir, name, x) {
|
||||
const enc = encodeURIComponent;
|
||||
if (!['docx', 'xlsx', 'pptx'].includes(x)) {
|
||||
await sf.req(`/api2/repos/${enc(repo)}/file/?p=${enc(joinP(dir, name))}`, { method: 'POST', headers: FORM, body: new URLSearchParams({ operation: 'create' }) });
|
||||
return name;
|
||||
}
|
||||
const tpl = await fetch('/templates/blank.' + x); if (!tpl.ok) throw new Error('Template missing');
|
||||
const link = await sf.req(`/api2/repos/${enc(repo)}/upload-link/?p=${enc(dir)}`);
|
||||
const fd = new FormData(); fd.append('parent_dir', dir); fd.append('file', await tpl.blob(), name);
|
||||
const r = await fetch('/sf' + new URL(link, cfg.seafile).pathname + '?ret-json=1', { method: 'POST', body: fd });
|
||||
if (!r.ok) throw new Error('Could not create ' + name);
|
||||
return ((await r.json())[0] || {}).name || name; // Seafile renames if the name is taken
|
||||
}
|
||||
|
||||
// Which library new Office files go in (remembered once chosen).
|
||||
async function pickLibrary() {
|
||||
const repos = (await sf.req('/api2/repos/')).filter(r => !r.encrypted && r.permission !== 'r');
|
||||
if (!repos.length) throw new Error('No library to create files in. Make one in Files first.');
|
||||
const saved = repos.find(r => r.id === store.get('office_repo'));
|
||||
if (saved) return saved;
|
||||
if (repos.length === 1) { store.set('office_repo', repos[0].id); return repos[0]; }
|
||||
return new Promise((ok, no) => actionSheet('Choose a library', 'New files will be created here', repos.map(r => ({ label: r.name, icon: 'files', run: () => { store.set('office_repo', r.id); ok(r); } })))
|
||||
|| 0);
|
||||
}
|
||||
|
||||
async function newOfficeDoc(x, label) {
|
||||
if (!sf.token()) { toast('Sign in to Files first'); location.hash = '#/files'; return; }
|
||||
let repo;
|
||||
try { repo = await pickLibrary(); } catch (e) { return toast(e.message); }
|
||||
textSheet('New ' + label, 'File name (in ' + repo.name + ')', 'Create', async n => {
|
||||
const want = n.toLowerCase().endsWith('.' + x) ? n : n + '.' + x;
|
||||
try {
|
||||
const name = await createFromTemplate(repo.id, '/', want, x), path = '/' + name;
|
||||
addRecent(repo.id, path, name);
|
||||
if (cfg.editor) location.hash = '#/edit/' + encodeURIComponent(repo.id) + '/' + encodeURIComponent(name);
|
||||
else window.open(`${cfg.seafile}/lib/${encodeURIComponent(repo.id)}/file/${encodeURIComponent(name)}`, '_blank', 'noopener');
|
||||
} catch (e) { toast(e.message); }
|
||||
}, 'Untitled');
|
||||
}
|
||||
|
||||
|
||||
// ---- 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: '' });
|
||||
@@ -706,9 +760,12 @@ async function loadDir(repo, dir) {
|
||||
{ label: 'Folder', icon: 'newdir', run: () => textSheet('New folder', 'Folder name', 'Create', n => create(joinP(dir, n), 'mkdir', {}, 'dir')) },
|
||||
...[['Document', 'docx'], ['Spreadsheet', 'xlsx'], ['Presentation', 'pptx'], ['Text file', 'txt']].map(([label, x]) => ({ label, icon: 'doc',
|
||||
run: () => textSheet('New ' + label.toLowerCase(), 'File name', 'Create', async n => {
|
||||
const name = n.toLowerCase().endsWith('.' + x) ? n : n + '.' + x;
|
||||
await create(joinP(dir, name), 'create');
|
||||
if (EDIT_EXT.has(x) && cfg.editor && x !== 'txt') openFile(repo, joinP(dir, name), name, dir);
|
||||
const want = n.toLowerCase().endsWith('.' + x) ? n : n + '.' + x;
|
||||
try {
|
||||
const name = await createFromTemplate(repo, dir, want, x);
|
||||
loadDir(repo, dir);
|
||||
if (x !== 'txt') openFile(repo, joinP(dir, name), name, dir);
|
||||
} catch (e) { toast(e.message); }
|
||||
}) })),
|
||||
]);
|
||||
$('#up').onchange = async e => {
|
||||
|
||||
Reference in new issue
Block a user