diff --git a/public/app.js b/public/app.js index f64cde3..4ba5b3d 100644 --- a/public/app.js +++ b/public/app.js @@ -319,15 +319,69 @@ function viewMail() { } function viewOffice() { - const url = cfg.onlyoffice; - $('#view').innerHTML = `

Office

-

OnlyOffice at ${esc(new URL(url).host)}. To edit a file from Seafile, open it in Files.

-
Open in new tab - Go to Files
- -

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.

`; + const docs = store.get('recents', []).filter(r => EDIT_EXT.has(ext(r.name)) && ext(r.name) !== 'txt').slice(0, 8); + $('#view').innerHTML = `

Office

Create a file in Seafile and edit it right here.

+
+ + + +
+

+ ${docs.length ? `

Recent documents

${docs.map((r, i) => ``).join('')}
` : ''} +
Browse all files
+

Editing server: ${esc(new URL(cfg.onlyoffice).host)}

`; + $('#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 ${esc(r.name)} ยท change` : '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 => { diff --git a/public/templates/blank.docx b/public/templates/blank.docx new file mode 100644 index 0000000..5507039 Binary files /dev/null and b/public/templates/blank.docx differ diff --git a/public/templates/blank.pptx b/public/templates/blank.pptx new file mode 100644 index 0000000..2521d21 Binary files /dev/null and b/public/templates/blank.pptx differ diff --git a/public/templates/blank.xlsx b/public/templates/blank.xlsx new file mode 100644 index 0000000..cc3eef2 Binary files /dev/null and b/public/templates/blank.xlsx differ