'use strict'; // Office: make, find and open documents, spreadsheets and presentations kept in Seafile (they open in OnlyOffice inside the app). // Search, a filter by kind, sorting, pinned and recently opened documents, a saved copy of the list that shows at once, and upload by button or by dropping files. const OFFICE_KINDS = { doc: { label: 'Documents', one: 'Document', color: '#3d6bf0', exts: ['docx', 'doc', 'odt', 'rtf'] }, sheet: { label: 'Spreadsheets', one: 'Spreadsheet', color: '#2f9e6e', exts: ['xlsx', 'xls', 'ods', 'csv'] }, slide: { label: 'Presentations', one: 'Presentation', color: '#e0742b', exts: ['pptx', 'ppt', 'odp'] }, }; const ofKind = n => Object.keys(OFFICE_KINDS).find(k => OFFICE_KINDS[k].exts.includes(ext(n))) || 'doc'; const ofIsDoc = n => EDIT_EXT.has(ext(n)) && ext(n) !== 'txt'; const ofState = { q: '', kind: 'all', sort: store.get('office_sort', 'mtime'), limit: 40, docs: [], loc: null }; const ofKey = loc => 'oc:' + (ME ? ME.id : '') + ':' + loc.repo + ':' + loc.dir; const ofPins = () => { const p = store.get('office_pins', []); return Array.isArray(p) ? p : []; }; const ofId = (repo, path) => repo + '|' + path; const ofPath = f => (f.parent_dir || '/').replace(/\/?$/, '/') + f.name; function ofOpen(repo, path, name) { addRecent(repo, path, name); if (cfg.editor) location.hash = officeHash(repo, path); else window.open(`${cfg.seafile}/lib/${encodeURIComponent(repo)}/file${encP(path)}`, '_blank', 'noopener'); } function ofIcon(name) { const k = OFFICE_KINDS[ofKind(name)]; return `${icon('doc')}${esc(ext(name).slice(0, 4).toUpperCase())}`; } function viewOffice(args = []) { if (args[0] === 'doc') return viewEdit(args.slice(1), { fromOffice: true }); const signed = !!sf.token(); $('#view').innerHTML = `

Office

${signed ? `` : ''}${icon('files')}All files
${Object.entries(OFFICE_KINDS).map(([k, o]) => ``).join('')}
${cfg.editor ? '' : '

Editing inside the app is switched off on this server, so documents open in Seafile instead.

'}

${signed ? 'Loading…' : 'Sign in to Files to create and open documents.'}

${signed ? '' : '
Sign in
'}
`; $('#view').querySelectorAll('.octile').forEach(t => t.onclick = () => newOfficeDoc(OFFICE_KINDS[t.dataset.k].exts[0], OFFICE_KINDS[t.dataset.k].one.toLowerCase())); if (!signed) return; $('#ofup').onclick = () => $('#ofupf').click(); $('#ofupf').onchange = e => { const files = [...e.target.files]; e.target.value = ''; if (files.length) ofUpload(files.map(file => ({ file, rel: '' }))); }; const wrap = $('#ofwrap'); wrap.ondragover = e => { if ([...(e.dataTransfer.types || [])].includes('Files')) { e.preventDefault(); wrap.classList.add('dropt'); } }; wrap.ondragleave = e => { if (!wrap.contains(e.relatedTarget)) wrap.classList.remove('dropt'); }; wrap.ondrop = async e => { e.preventDefault(); wrap.classList.remove('dropt'); const files = (await droppedFiles(e.dataTransfer)).filter(f => ofIsDoc(f.file.name)); if (files.length) ofUpload(files); else toast('Drop Word, Excel or PowerPoint files here'); }; $('#oq').oninput = e => { ofState.q = e.target.value; ofState.limit = 40; ofDraw(); }; $('#osort').value = ofState.sort; $('#osort').onchange = e => { ofState.sort = e.target.value; store.set('office_sort', ofState.sort); ofDraw(); }; ofLoad(); } async function ofUpload(entries) { const loc = ofState.loc || await officeLocation(true); if (!loc) return; await enqueueUploads(loc.repo, loc.dir, entries); const t = setInterval(() => { if (!upActive()) { clearInterval(t); if (location.hash.startsWith('#/office') && $('#odocs')) ofLoad(true); } }, 1200); setTimeout(() => clearInterval(t), 10 * 60e3); } async function ofLoad(quiet) { try { const loc = await officeLocation(false), el = $('#olib'); if (!el) return; if (!loc) { el.textContent = 'Choose where your documents are saved in Seafile.'; $('#odocs').innerHTML = '
'; $('#chl2').onclick = () => chooseFolder('Save documents in…').then(l => { if (l) { store.set('office_loc', l); viewOffice(); } }); return; } ofState.loc = loc; el.innerHTML = `Saved in ${esc(locLabel(loc))} · change`; $('#chl').onclick = e => { e.preventDefault(); chooseFolder('Save documents in…', loc).then(l => { if (l) { store.set('office_loc', l); ofState.docs = []; viewOffice(); } }); }; let saved = null; try { saved = JSON.parse(localStorage.getItem(ofKey(loc))); } catch {} if (!quiet && saved && saved.docs) { ofState.docs = saved.docs; ofDraw(); } else if (!quiet) ofDraw(true); const all = await sf.req(`/api2/repos/${encodeURIComponent(loc.repo)}/dir/?p=${encodeURIComponent(loc.dir)}&recursive=1`); if (!location.hash.startsWith('#/office') || !$('#odocs')) return; ofState.docs = all.filter(f => f.type === 'file' && ofIsDoc(f.name)).map(f => ({ name: f.name, parent_dir: f.parent_dir || '/', mtime: f.mtime, size: f.size || 0 })); try { localStorage.setItem(ofKey(loc), JSON.stringify({ t: Date.now(), docs: ofState.docs.slice(0, 400) })); } catch {} ofDraw(); } catch (e) { const el = $('#olib'); if (!el) return; if (ofState.docs.length) { el.innerHTML += ` · ${esc(navigator.onLine ? e.message : 'offline')} (showing the saved list)`; ofDraw(); } else { el.innerHTML = `${esc(e.message)} Try again`; $('#ofretry').onclick = ev => { ev.preventDefault(); ofLoad(); }; } } } function ofDraw(skeleton) { const box = $('#odocs'), loc = ofState.loc; if (!box || !loc) return; $('#oflt').hidden = !ofState.docs.length; const pins = new Set(ofPins()), q = ofState.q.trim().toLowerCase(), counts = { all: ofState.docs.length }; for (const k of Object.keys(OFFICE_KINDS)) counts[k] = ofState.docs.filter(d => ofKind(d.name) === k).length; $('#okinds').innerHTML = [['all', 'All']].concat(Object.entries(OFFICE_KINDS).map(([k, o]) => [k, o.label])).filter(([k]) => k === 'all' || counts[k]).map(([k, l]) => `${l}`).join(''); $('#okinds').querySelectorAll('a').forEach(a => a.onclick = e => { e.preventDefault(); ofState.kind = a.dataset.k; ofState.limit = 40; ofDraw(); }); const cmp = { mtime: (a, b) => b.mtime - a.mtime, name: (a, b) => collator.compare(a.name, b.name), size: (a, b) => b.size - a.size }[ofState.sort] || ((a, b) => b.mtime - a.mtime); const match = d => (ofState.kind === 'all' || ofKind(d.name) === ofState.kind) && (!q || (d.name + ' ' + d.parent_dir).toLowerCase().includes(q)); const list = ofState.docs.filter(match).sort(cmp), shown = list.slice(0, ofState.limit), pinned = shown.filter(d => pins.has(ofId(loc.repo, ofPath(d)))); const recents = !q && ofState.kind === 'all' ? store.get('recents', []).filter(r => r.repo === loc.repo && ofIsDoc(r.name)).slice(0, 6) : []; const row = d => { const path = ofPath(d), on = pins.has(ofId(loc.repo, path)), i = shown.indexOf(d); return `
`; }; box.innerHTML = skeleton ? '

Loading your documents…

' : (recents.length ? `

Recently opened

${recents.map((r, i) => ``).join('')}
` : '') + (pinned.length && !q ? `

Pinned

${pinned.map(row).join('')}
` : '') + (list.length ? `

${q ? 'Results' : ofState.kind === 'all' ? 'All documents' : OFFICE_KINDS[ofState.kind].label} ${list.length}

${shown.map(row).join('')}
${list.length > shown.length ? `
` : ''}` : `

${ofState.docs.length ? 'Nothing matches.' : 'No documents here yet. Create one above, or drop files on this page.'}

`); box.querySelectorAll('[data-o]').forEach(b => b.onclick = () => { const d = shown[+b.dataset.o]; if (d) ofOpen(loc.repo, ofPath(d), d.name); }); box.querySelectorAll('[data-m]').forEach(b => b.onclick = () => { const d = shown[+b.dataset.m]; if (d) fileSheet(loc.repo, ofPath(d), d.name, d.parent_dir === '/' ? '/' : d.parent_dir.replace(/\/$/, '')); }); box.querySelectorAll('[data-pin]').forEach(b => b.onclick = () => { const d = shown[+b.dataset.pin]; if (!d) return; const id = ofId(loc.repo, ofPath(d)), cur = ofPins(); store.set('office_pins', cur.includes(id) ? cur.filter(x => x !== id) : [id, ...cur].slice(0, 50)); ofDraw(); }); box.querySelectorAll('[data-r]').forEach(b => b.onclick = () => { const r = recents[+b.dataset.r]; if (r) ofOpen(r.repo, r.path, r.name); }); const more = $('#ofmore'); if (more) more.onclick = () => { ofState.limit += 40; ofDraw(); }; }