feat: previews of PDF, Word, spreadsheets, CSV and HTML in the file manager; the find bar's buttons back in one row
This commit is contained in:
1 parent
d448c1e3a4
commit
68ede6ab25
3 files changed
+513
-18
No files matched your search
@@ -199,10 +199,13 @@ var inlineTypes = map[string]string{
|
|||||||
".mp4": "video/mp4", ".m4v": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg", ".mov": "video/quicktime",
|
".mp4": "video/mp4", ".m4v": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg", ".mov": "video/quicktime",
|
||||||
".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg", ".oga": "audio/ogg", ".m4a": "audio/mp4",
|
".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg", ".oga": "audio/ogg", ".m4a": "audio/mp4",
|
||||||
".aac": "audio/aac", ".flac": "audio/flac", ".opus": "audio/ogg",
|
".aac": "audio/aac", ".flac": "audio/flac", ".opus": "audio/ogg",
|
||||||
|
".pdf": "application/pdf",
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileHeaders sets what the browser needs to download or show the file. A
|
// fileHeaders sets what the browser needs to download or show the file. A
|
||||||
// file shown in place runs nothing: no scripts in an SVG, whatever it holds.
|
// file shown in place runs nothing: no scripts in an SVG, whatever it holds.
|
||||||
|
// A PDF is the exception, as the browser's viewer will not open in a sandbox;
|
||||||
|
// it runs in that viewer, not in the panel's page.
|
||||||
func fileHeaders(w http.ResponseWriter, p string, inline bool) {
|
func fileHeaders(w http.ResponseWriter, p string, inline bool) {
|
||||||
name := path.Base(p)
|
name := path.Base(p)
|
||||||
ctype := "application/octet-stream"
|
ctype := "application/octet-stream"
|
||||||
@@ -216,7 +219,9 @@ func fileHeaders(w http.ResponseWriter, p string, inline bool) {
|
|||||||
w.Header().Set("Content-Type", ctype)
|
w.Header().Set("Content-Type", ctype)
|
||||||
w.Header().Set("Content-Disposition", disp+"; filename=\""+strings.ReplaceAll(name, "\"", "")+"\"")
|
w.Header().Set("Content-Disposition", disp+"; filename=\""+strings.ReplaceAll(name, "\"", "")+"\"")
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
if ctype != "application/pdf" {
|
||||||
w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'")
|
w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; img-src data:; style-src 'unsafe-inline'")
|
||||||
|
}
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
// Light previews of office files for the file manager, with no libraries:
|
||||||
|
// docx, xlsx and ods are zip archives of XML, opened with the browser's own
|
||||||
|
// DecompressionStream; csv and tsv are parsed here; an old binary .doc only
|
||||||
|
// gives up its text. Each turns into plain HTML the page styles itself.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c]));
|
||||||
|
|
||||||
|
// ---------- zip ----------
|
||||||
|
// Reads the central directory; a file is inflated only when asked for.
|
||||||
|
function openZip(buf) {
|
||||||
|
const b = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
||||||
|
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
||||||
|
let eocd = -1;
|
||||||
|
for (let i = b.length - 22; i >= Math.max(0, b.length - 65557); i--) {
|
||||||
|
if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
|
||||||
|
}
|
||||||
|
if (eocd < 0) throw new Error('не zip-архив');
|
||||||
|
const count = dv.getUint16(eocd + 10, true);
|
||||||
|
let p = dv.getUint32(eocd + 16, true);
|
||||||
|
const files = new Map();
|
||||||
|
const dec = new TextDecoder();
|
||||||
|
for (let n = 0; n < count && dv.getUint32(p, true) === 0x02014b50; n++) {
|
||||||
|
const method = dv.getUint16(p + 10, true), size = dv.getUint32(p + 20, true);
|
||||||
|
const nameLen = dv.getUint16(p + 28, true), extra = dv.getUint16(p + 30, true), comment = dv.getUint16(p + 32, true);
|
||||||
|
const local = dv.getUint32(p + 42, true);
|
||||||
|
files.set(dec.decode(b.subarray(p + 46, p + 46 + nameLen)), {method, size, local});
|
||||||
|
p += 46 + nameLen + extra + comment;
|
||||||
|
}
|
||||||
|
async function bytes(name) {
|
||||||
|
const f = files.get(name);
|
||||||
|
if (!f) return null;
|
||||||
|
const start = f.local + 30 + dv.getUint16(f.local + 26, true) + dv.getUint16(f.local + 28, true);
|
||||||
|
const raw = b.subarray(start, start + f.size);
|
||||||
|
if (f.method === 0) return raw;
|
||||||
|
if (f.method !== 8) throw new Error('сжатие zip, которое браузер не знает');
|
||||||
|
const out = new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
|
||||||
|
return new Uint8Array(await new Response(out).arrayBuffer());
|
||||||
|
}
|
||||||
|
async function xml(name) {
|
||||||
|
const raw = await bytes(name);
|
||||||
|
return raw ? new DOMParser().parseFromString(new TextDecoder().decode(raw), 'application/xml') : null;
|
||||||
|
}
|
||||||
|
return {files, bytes, xml};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children of an element by local name, whatever the namespace prefix.
|
||||||
|
const kids = (el, name) => el ? Array.from(el.children).filter(c => c.localName === name) : [];
|
||||||
|
const kid = (el, name) => kids(el, name)[0] || null;
|
||||||
|
const attr = (el, name) => {
|
||||||
|
if (!el) return null;
|
||||||
|
for (const a of el.attributes) if (a.localName === name) return a.value;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- docx ----------
|
||||||
|
async function docx(buf) {
|
||||||
|
const z = openZip(buf);
|
||||||
|
const doc = await z.xml('word/document.xml');
|
||||||
|
if (!doc) throw new Error('в архиве нет word/document.xml');
|
||||||
|
// Style ids to names, to know headings whatever the language of Word.
|
||||||
|
const styles = {};
|
||||||
|
const st = await z.xml('word/styles.xml');
|
||||||
|
if (st) for (const s of st.getElementsByTagNameNS('*', 'style')) {
|
||||||
|
styles[attr(s, 'styleId')] = (attr(kid(s, 'name'), 'val') || '').toLowerCase();
|
||||||
|
}
|
||||||
|
const body = doc.getElementsByTagNameNS('*', 'body')[0];
|
||||||
|
return '<div class="dv-doc">' + blocks(body, styles) + '</div>';
|
||||||
|
}
|
||||||
|
function blocks(el, styles) {
|
||||||
|
let html = '', list = false;
|
||||||
|
for (const c of el ? el.children : []) {
|
||||||
|
if (c.localName === 'p') {
|
||||||
|
const pPr = kid(c, 'pPr');
|
||||||
|
const isList = !!kid(pPr, 'numPr');
|
||||||
|
if (isList && !list) { html += '<ul>'; list = true; }
|
||||||
|
if (!isList && list) { html += '</ul>'; list = false; }
|
||||||
|
html += para(c, pPr, styles, isList);
|
||||||
|
} else {
|
||||||
|
if (list) { html += '</ul>'; list = false; }
|
||||||
|
if (c.localName === 'tbl') html += table(c, styles);
|
||||||
|
else if (c.localName === 'sdt') html += blocks(kid(c, 'sdtContent'), styles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html + (list ? '</ul>' : '');
|
||||||
|
}
|
||||||
|
function para(p, pPr, styles, isList) {
|
||||||
|
const name = styles[attr(kid(pPr, 'pStyle'), 'val')] || '';
|
||||||
|
const h = /^heading (\d)/.exec(name);
|
||||||
|
const tag = isList ? 'li' : name === 'title' ? 'h1' : h ? 'h' + Math.min(6, +h[1] + 1) : 'p';
|
||||||
|
const jc = attr(kid(pPr, 'jc'), 'val');
|
||||||
|
const align = jc === 'center' ? 'center' : (jc === 'right' || jc === 'end') ? 'right' : (jc === 'both' ? 'justify' : '');
|
||||||
|
const inner = runs(p);
|
||||||
|
return '<' + tag + (align ? ' style="text-align:' + align + '"' : '') + '>' + (inner || ' ') + '</' + tag + '>';
|
||||||
|
}
|
||||||
|
function runs(el) {
|
||||||
|
let html = '';
|
||||||
|
for (const c of el.children) {
|
||||||
|
if (c.localName === 'r') html += run(c);
|
||||||
|
else if (c.localName === 'hyperlink' || c.localName === 'smartTag' || c.localName === 'ins' || c.localName === 'sdt' || c.localName === 'sdtContent' || c.localName === 'fldSimple') {
|
||||||
|
const inner = runs(c);
|
||||||
|
html += c.localName === 'hyperlink' ? '<u class="dv-link">' + inner + '</u>' : inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
function run(r) {
|
||||||
|
const pr = kid(r, 'rPr');
|
||||||
|
const on = n => { const e = kid(pr, n); return !!e && attr(e, 'val') !== '0' && attr(e, 'val') !== 'false'; };
|
||||||
|
let t = '';
|
||||||
|
for (const c of r.children) {
|
||||||
|
if (c.localName === 't') t += esc(c.textContent);
|
||||||
|
else if (c.localName === 'tab') t += ' ';
|
||||||
|
else if (c.localName === 'br' || c.localName === 'cr') t += '<br>';
|
||||||
|
}
|
||||||
|
if (!t) return '';
|
||||||
|
if (on('b')) t = '<b>' + t + '</b>';
|
||||||
|
if (on('i')) t = '<i>' + t + '</i>';
|
||||||
|
if (kid(pr, 'u') && attr(kid(pr, 'u'), 'val') !== 'none') t = '<u>' + t + '</u>';
|
||||||
|
if (on('strike')) t = '<s>' + t + '</s>';
|
||||||
|
// A size set on the run itself, as many documents do instead of styles.
|
||||||
|
const sz = +attr(kid(pr, 'sz'), 'val');
|
||||||
|
if (sz && sz !== 24) t = '<span style="font-size:' + Math.max(9, Math.min(36, sz / 2)) + 'pt">' + t + '</span>';
|
||||||
|
const va = attr(kid(pr, 'vertAlign'), 'val');
|
||||||
|
if (va === 'superscript') t = '<sup>' + t + '</sup>';
|
||||||
|
else if (va === 'subscript') t = '<sub>' + t + '</sub>';
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
function table(tbl, styles) {
|
||||||
|
let html = '<div class="dv-scroll"><table class="dv-table">';
|
||||||
|
for (const tr of kids(tbl, 'tr')) {
|
||||||
|
html += '<tr>';
|
||||||
|
for (const tc of kids(tr, 'tc')) {
|
||||||
|
const span = attr(kid(kid(tc, 'tcPr'), 'gridSpan'), 'val');
|
||||||
|
html += '<td' + (span > 1 ? ' colspan="' + span + '"' : '') + '>' + blocks(tc, styles) + '</td>';
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
return html + '</table></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- tables: one grid per sheet ----------
|
||||||
|
const MAX_ROWS = 2000, MAX_COLS = 60;
|
||||||
|
function colName(i) { let s = ''; for (i++; i > 0; i = Math.floor((i - 1) / 26)) s = String.fromCharCode(65 + (i - 1) % 26) + s; return s; }
|
||||||
|
function grid(rows) {
|
||||||
|
let cols = 0;
|
||||||
|
for (const r of rows) cols = Math.max(cols, r.length);
|
||||||
|
cols = Math.min(cols, MAX_COLS);
|
||||||
|
const cut = rows.length > MAX_ROWS || rows.some(r => r.length > MAX_COLS);
|
||||||
|
let html = '<div class="dv-scroll dv-grid"><table class="dv-sheet"><thead><tr><th></th>';
|
||||||
|
for (let c = 0; c < cols; c++) html += '<th>' + colName(c) + '</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
rows.slice(0, MAX_ROWS).forEach((r, i) => {
|
||||||
|
html += '<tr><th>' + (i + 1) + '</th>';
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const v = r[c] == null ? '' : r[c];
|
||||||
|
html += '<td' + (typeof v === 'number' ? ' class="n"' : '') + '>' + esc(v) + '</td>';
|
||||||
|
}
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
return {html, cut};
|
||||||
|
}
|
||||||
|
// Several sheets: tabs over the grid.
|
||||||
|
function sheets(list) {
|
||||||
|
if (!list.length) return '<p class="dv-note">в книге нет листов</p>';
|
||||||
|
let html = '';
|
||||||
|
if (list.length > 1) {
|
||||||
|
html += '<div class="dv-tabs">' + list.map((s, i) => '<button type="button" data-sheet="' + i + '"' + (i ? '' : ' class="on"') + '>' + esc(s.name) + '</button>').join('') + '</div>';
|
||||||
|
}
|
||||||
|
list.forEach((s, i) => {
|
||||||
|
const g = grid(s.rows);
|
||||||
|
html += '<div class="dv-pane" data-pane="' + i + '"' + (i ? ' hidden' : '') + '>' + g.html +
|
||||||
|
(g.cut ? '<p class="dv-note">показаны первые ' + MAX_ROWS + ' строк и ' + MAX_COLS + ' столбцов</p>' : '') + '</div>';
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
function bindTabs(root) {
|
||||||
|
root.querySelectorAll('[data-sheet]').forEach(b => b.onclick = () => {
|
||||||
|
root.querySelectorAll('[data-sheet]').forEach(x => x.classList.toggle('on', x === b));
|
||||||
|
root.querySelectorAll('[data-pane]').forEach(p => p.hidden = p.dataset.pane !== b.dataset.sheet);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- xlsx ----------
|
||||||
|
async function xlsx(buf) {
|
||||||
|
const z = openZip(buf);
|
||||||
|
const wb = await z.xml('xl/workbook.xml');
|
||||||
|
if (!wb) throw new Error('в архиве нет xl/workbook.xml');
|
||||||
|
const rels = {};
|
||||||
|
const rx = await z.xml('xl/_rels/workbook.xml.rels');
|
||||||
|
if (rx) for (const r of rx.getElementsByTagNameNS('*', 'Relationship')) rels[r.getAttribute('Id')] = r.getAttribute('Target');
|
||||||
|
const shared = [];
|
||||||
|
const ss = await z.xml('xl/sharedStrings.xml');
|
||||||
|
if (ss) for (const si of ss.getElementsByTagNameNS('*', 'si')) {
|
||||||
|
shared.push(Array.from(si.getElementsByTagNameNS('*', 't')).filter(t => t.parentElement.localName !== 'rPh').map(t => t.textContent).join(''));
|
||||||
|
}
|
||||||
|
const list = [];
|
||||||
|
for (const s of wb.getElementsByTagNameNS('*', 'sheet')) {
|
||||||
|
let target = rels[attr(s, 'id')] || '';
|
||||||
|
target = target.startsWith('/') ? target.slice(1) : 'xl/' + target.replace(/^\.\//, '');
|
||||||
|
const doc = await z.xml(target);
|
||||||
|
list.push({name: s.getAttribute('name') || '?', rows: doc ? xlsxRows(doc, shared) : []});
|
||||||
|
}
|
||||||
|
return sheets(list);
|
||||||
|
}
|
||||||
|
function xlsxRows(doc, shared) {
|
||||||
|
const rows = [];
|
||||||
|
for (const row of doc.getElementsByTagNameNS('*', 'row')) {
|
||||||
|
const ri = (+row.getAttribute('r') || rows.length + 1) - 1;
|
||||||
|
if (ri >= MAX_ROWS) break;
|
||||||
|
const out = rows[ri] = rows[ri] || [];
|
||||||
|
let next = 0;
|
||||||
|
for (const c of kids(row, 'c')) {
|
||||||
|
const ref = /^([A-Z]+)/.exec(c.getAttribute('r') || '');
|
||||||
|
let ci = next;
|
||||||
|
if (ref) { ci = 0; for (const ch of ref[1]) ci = ci * 26 + ch.charCodeAt(0) - 64; ci--; }
|
||||||
|
next = ci + 1;
|
||||||
|
if (ci >= MAX_COLS + 1) continue;
|
||||||
|
const t = c.getAttribute('t'), v = kid(c, 'v');
|
||||||
|
let val = v ? v.textContent : '';
|
||||||
|
if (t === 's') val = shared[+val] || '';
|
||||||
|
else if (t === 'inlineStr') val = Array.from(c.getElementsByTagNameNS('*', 't')).map(x => x.textContent).join('');
|
||||||
|
else if (t === 'b') val = val === '1' ? 'TRUE' : 'FALSE';
|
||||||
|
else if (t !== 'str' && t !== 'e' && val !== '' && !isNaN(val)) val = +val;
|
||||||
|
out[ci] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < rows.length; i++) rows[i] = rows[i] || [];
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- ods / odt ----------
|
||||||
|
async function ods(buf) {
|
||||||
|
const z = openZip(buf);
|
||||||
|
const doc = await z.xml('content.xml');
|
||||||
|
if (!doc) throw new Error('в архиве нет content.xml');
|
||||||
|
const list = [];
|
||||||
|
for (const t of doc.getElementsByTagNameNS('*', 'table')) {
|
||||||
|
if (t.parentElement && t.parentElement.localName === 'table-cell') continue;
|
||||||
|
const rows = [];
|
||||||
|
for (const r of t.getElementsByTagNameNS('*', 'table-row')) {
|
||||||
|
const cells = [];
|
||||||
|
for (const c of r.children) {
|
||||||
|
if (c.localName !== 'table-cell' && c.localName !== 'covered-table-cell') continue;
|
||||||
|
const rep = Math.min(+attr(c, 'number-columns-repeated') || 1, MAX_COLS + 1 - cells.length);
|
||||||
|
const num = attr(c, 'value');
|
||||||
|
const text = Array.from(c.getElementsByTagNameNS('*', 'p')).map(p => p.textContent).join('\n');
|
||||||
|
const val = attr(c, 'value-type') === 'float' && num != null ? +num : text;
|
||||||
|
for (let i = 0; i < rep; i++) cells.push(val);
|
||||||
|
if (cells.length > MAX_COLS) break;
|
||||||
|
}
|
||||||
|
while (cells.length && cells[cells.length - 1] === '') cells.pop();
|
||||||
|
const rep = Math.min(+attr(r, 'number-rows-repeated') || 1, MAX_ROWS + 1 - rows.length);
|
||||||
|
for (let i = 0; i < rep; i++) rows.push(cells);
|
||||||
|
if (rows.length > MAX_ROWS) break;
|
||||||
|
}
|
||||||
|
while (rows.length && !rows[rows.length - 1].length) rows.pop();
|
||||||
|
list.push({name: attr(t, 'name') || '?', rows});
|
||||||
|
}
|
||||||
|
return sheets(list);
|
||||||
|
}
|
||||||
|
async function odt(buf) {
|
||||||
|
const z = openZip(buf);
|
||||||
|
const doc = await z.xml('content.xml');
|
||||||
|
const body = doc && doc.getElementsByTagNameNS('*', 'text')[0];
|
||||||
|
if (!body) throw new Error('в архиве нет текста');
|
||||||
|
// Runs of spaces, tabs and breaks are elements of their own in ODF.
|
||||||
|
const inline = el => {
|
||||||
|
let html = '';
|
||||||
|
for (const n of el.childNodes) {
|
||||||
|
if (n.nodeType === 3) html += esc(n.nodeValue);
|
||||||
|
else if (n.localName === 's') html += ' '.repeat(+attr(n, 'c') || 1);
|
||||||
|
else if (n.localName === 'tab') html += ' ';
|
||||||
|
else if (n.localName === 'line-break') html += '<br>';
|
||||||
|
else if (n.nodeType === 1) html += inline(n);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
const walk = el => {
|
||||||
|
let html = '';
|
||||||
|
for (const c of el.children) {
|
||||||
|
if (c.localName === 'h') { const l = Math.min(6, (+attr(c, 'outline-level') || 1) + 1); html += '<h' + l + '>' + inline(c) + '</h' + l + '>'; }
|
||||||
|
else if (c.localName === 'p') html += '<p>' + (inline(c) || ' ') + '</p>';
|
||||||
|
else if (c.localName === 'list') html += '<ul>' + kids(c, 'list-item').map(li => '<li>' + walk(li) + '</li>').join('') + '</ul>';
|
||||||
|
else if (c.localName === 'table') html += '<div class="dv-scroll"><table class="dv-table">' + Array.from(c.getElementsByTagNameNS('*', 'table-row')).map(r =>
|
||||||
|
'<tr>' + kids(r, 'table-cell').map(td => '<td>' + walk(td) + '</td>').join('') + '</tr>').join('') + '</table></div>';
|
||||||
|
else if (c.localName === 'section') html += walk(c);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
return '<div class="dv-doc">' + walk(body) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- csv / tsv ----------
|
||||||
|
function csv(text, tab) {
|
||||||
|
const first = text.slice(0, text.indexOf('\n') >>> 0 || text.length);
|
||||||
|
const sep = tab ? '\t' : (first.split(';').length > first.split(',').length ? ';' : ',');
|
||||||
|
const rows = [];
|
||||||
|
let row = [], f = '', q = false;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const c = text[i];
|
||||||
|
if (q) {
|
||||||
|
if (c === '"') { if (text[i + 1] === '"') { f += '"'; i++; } else q = false; }
|
||||||
|
else f += c;
|
||||||
|
} else if (c === '"' && f === '') q = true;
|
||||||
|
else if (c === sep) { row.push(f); f = ''; }
|
||||||
|
else if (c === '\n' || c === '\r') {
|
||||||
|
if (c === '\r' && text[i + 1] === '\n') i++;
|
||||||
|
row.push(f); f = ''; rows.push(row); row = [];
|
||||||
|
if (rows.length > MAX_ROWS) break;
|
||||||
|
} else f += c;
|
||||||
|
}
|
||||||
|
if (f !== '' || row.length) { row.push(f); rows.push(row); }
|
||||||
|
return sheets([{name: '', rows: rows.map(r => r.map(v => v !== '' && !isNaN(v) && /^-?[\d.]+$/.test(v.trim()) ? +v : v))}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- old .doc ----------
|
||||||
|
// Word 97-2003 is a binary format; without a full parser all that can be
|
||||||
|
// shown is the text, picked out of the document's WordDocument stream as
|
||||||
|
// the runs that read as words.
|
||||||
|
function cfbStream(b, want) {
|
||||||
|
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
||||||
|
if (dv.getUint32(0, true) !== 0xE011CFD0) return null;
|
||||||
|
const ss = 1 << dv.getUint16(0x1E, true);
|
||||||
|
const sector = n => 512 + n * ss;
|
||||||
|
const fat = [];
|
||||||
|
const addFat = n => { const o = sector(n); for (let i = 0; i < ss / 4; i++) fat.push(dv.getUint32(o + i * 4, true)); };
|
||||||
|
for (let i = 0; i < 109; i++) { const n = dv.getUint32(0x4C + i * 4, true); if (n < 0xFFFFFFFA) addFat(n); }
|
||||||
|
for (let n = dv.getUint32(0x44, true), k = 0; n < 0xFFFFFFFA && k < 1000; k++) {
|
||||||
|
const o = sector(n);
|
||||||
|
for (let i = 0; i < ss / 4 - 1; i++) { const m = dv.getUint32(o + i * 4, true); if (m < 0xFFFFFFFA) addFat(m); }
|
||||||
|
n = dv.getUint32(o + ss - 4, true);
|
||||||
|
}
|
||||||
|
const chain = (start, size) => {
|
||||||
|
const out = new Uint8Array(size);
|
||||||
|
let n = start, at = 0;
|
||||||
|
for (let guard = 0; n < 0xFFFFFFFA && at < size && guard < 1e6; guard++) {
|
||||||
|
const o = sector(n), len = Math.min(ss, size - at);
|
||||||
|
if (o + len > b.length) break;
|
||||||
|
out.set(b.subarray(o, o + len), at);
|
||||||
|
at += len;
|
||||||
|
n = fat[n];
|
||||||
|
}
|
||||||
|
return out.subarray(0, at);
|
||||||
|
};
|
||||||
|
const dirStart = dv.getUint32(0x30, true);
|
||||||
|
const dir = chain(dirStart, ss * 64);
|
||||||
|
const dd = new DataView(dir.buffer, dir.byteOffset, dir.byteLength);
|
||||||
|
for (let o = 0; o + 128 <= dir.length; o += 128) {
|
||||||
|
const nameLen = dd.getUint16(o + 0x40, true);
|
||||||
|
let name = '';
|
||||||
|
for (let i = 0; i + 2 < nameLen; i += 2) name += String.fromCharCode(dd.getUint16(o + i, true));
|
||||||
|
if (name === want && dd.getUint8(o + 0x42) === 2) return chain(dd.getUint32(o + 0x74, true), dd.getUint32(o + 0x78, true));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function doc(buf) {
|
||||||
|
const all = new Uint8Array(buf);
|
||||||
|
const b = cfbStream(all, 'WordDocument') || all;
|
||||||
|
const ok = c => c === 9 || c === 10 || c === 13 || (c >= 32 && c < 127) || (c >= 0xA0 && c <= 0x24F) || (c >= 0x400 && c <= 0x4FF) || (c >= 0x2010 && c <= 0x2026) || c === 0x2116;
|
||||||
|
const pieces = [];
|
||||||
|
let cur = '';
|
||||||
|
const flush = min => {
|
||||||
|
const letters = (cur.match(/[A-Za-zА-Яа-яЁё0-9]/g) || []).length;
|
||||||
|
if (cur.trim().length >= min && letters >= cur.trim().length * 0.5) pieces.push(cur);
|
||||||
|
cur = '';
|
||||||
|
};
|
||||||
|
for (let i = 0; i + 1 < b.length; i += 2) {
|
||||||
|
const c = b[i] | (b[i + 1] << 8);
|
||||||
|
if (ok(c)) cur += String.fromCharCode(c); else flush(3);
|
||||||
|
}
|
||||||
|
flush(4);
|
||||||
|
let text = pieces.join('\n');
|
||||||
|
if (text.length < 40) { // a document that keeps its text 8-bit
|
||||||
|
const p8 = [];
|
||||||
|
for (let i = 0; i < b.length; i++) {
|
||||||
|
const c = b[i];
|
||||||
|
if (c === 9 || c === 10 || c === 13 || (c >= 32 && c < 127)) cur += String.fromCharCode(c);
|
||||||
|
else { if (cur.trim().length >= 8 && (cur.match(/[A-Za-z]/g) || []).length >= cur.length * 0.5) p8.push(cur); cur = ''; }
|
||||||
|
}
|
||||||
|
text = p8.join('\n');
|
||||||
|
}
|
||||||
|
if (!text.trim()) throw new Error('текст не нашёлся');
|
||||||
|
return '<p class="dv-note">старый формат .doc: показан только текст, без оформления</p><div class="dv-doc">' +
|
||||||
|
text.split(/\r\n|\r|\n|\x07/).filter(l => l.trim()).map(l => '<p>' + esc(l) + '</p>').join('') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DocView = {docx, xlsx, ods, odt, csv, doc, bindTabs};
|
||||||
|
})();
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
background:#0c0f13 repeating-conic-gradient(#151a21 0 25%, #0c0f13 0 50%) 0 0/20px 20px; border-radius:0 0 12px 12px; }
|
background:#0c0f13 repeating-conic-gradient(#151a21 0 25%, #0c0f13 0 50%) 0 0/20px 20px; border-radius:0 0 12px 12px; }
|
||||||
.vmedia img, .vmedia video { max-width:100%; max-height:calc(75vh - 32px); object-fit:contain; }
|
.vmedia img, .vmedia video { max-width:100%; max-height:calc(75vh - 32px); object-fit:contain; }
|
||||||
.vmedia audio { width:min(100%, 520px); }
|
.vmedia audio { width:min(100%, 520px); }
|
||||||
/* Find and replace, over the editor: both fields the same width, their
|
/* Find and replace, over the editor: both fields the same width, the
|
||||||
buttons in a column of their own, the close button beside both rows. */
|
buttons of each row after its field. */
|
||||||
.find { display:grid; grid-template-columns:minmax(0, 1fr) auto auto; gap:6px 8px; align-items:center;
|
.find { display:grid; grid-template-columns:minmax(0, 1fr) auto; gap:6px 8px; align-items:center;
|
||||||
padding:8px 12px; border-bottom:1px solid var(--border); background:var(--panel); }
|
padding:8px 12px; border-bottom:1px solid var(--border); background:var(--panel); }
|
||||||
.find .fin { position:relative; grid-column:1; }
|
.find .fin { position:relative; grid-column:1; }
|
||||||
.find input { width:100%; height:32px; padding:4px 76px 4px 10px; font-size:13px; font-family:ui-monospace,Menlo,monospace; }
|
.find input { width:100%; height:32px; padding:4px 76px 4px 10px; font-size:13px; font-family:ui-monospace,Menlo,monospace; }
|
||||||
@@ -85,16 +85,40 @@
|
|||||||
.find input.bad { border-color:var(--danger); }
|
.find input.bad { border-color:var(--danger); }
|
||||||
.find .cnt { position:absolute; right:10px; top:0; bottom:0; display:flex; align-items:center; pointer-events:none;
|
.find .cnt { position:absolute; right:10px; top:0; bottom:0; display:flex; align-items:center; pointer-events:none;
|
||||||
color:var(--muted); font-size:12px; font-variant-numeric:tabular-nums; }
|
color:var(--muted); font-size:12px; font-variant-numeric:tabular-nums; }
|
||||||
.find .fbtns { grid-column:2; display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:6px; }
|
.find .fbtns { grid-column:2; justify-self:end; display:flex; gap:6px; }
|
||||||
.find button { height:32px; min-width:32px; padding:0 10px; font-size:13px; }
|
.find button { height:32px; min-width:32px; padding:0 10px; font-size:13px;
|
||||||
|
display:inline-flex; align-items:center; justify-content:center; }
|
||||||
.find button.tg { background:transparent; border:1px solid var(--border); color:var(--muted); font-family:ui-monospace,Menlo,monospace; }
|
.find button.tg { background:transparent; border:1px solid var(--border); color:var(--muted); font-family:ui-monospace,Menlo,monospace; }
|
||||||
.find button.tg.on { border-color:var(--accent); color:var(--text); background:#12202f; }
|
.find button.tg.on { border-color:var(--accent); color:var(--text); background:#12202f; }
|
||||||
.find .fx { grid-column:3; grid-row:1 / span 2; align-self:center; width:32px; height:32px; padding:0;
|
.find .fx { color:var(--danger); }
|
||||||
display:inline-flex; align-items:center; justify-content:center;
|
|
||||||
background:transparent; border:1px solid rgba(255,92,92,.55); color:var(--danger); }
|
|
||||||
.find .fx:hover { background:#3a1d1d; border-color:var(--danger); }
|
|
||||||
.find.ro .fx { grid-row:1; }
|
|
||||||
.find #frow2, .find #frow2b { grid-row:2; }
|
.find #frow2, .find #frow2b { grid-row:2; }
|
||||||
|
/* Documents and tables, drawn in the panel's colours. */
|
||||||
|
.vdoc { height:75vh; overflow:auto; background:#0c0f13; border-radius:0 0 12px 12px; }
|
||||||
|
.vdoc .dv-pdf { display:block; width:100%; height:100%; border:0; background:#525659; }
|
||||||
|
.vdoc .dv-html { display:block; width:100%; height:100%; border:0; background:#fff; }
|
||||||
|
.dv-doc { max-width:820px; margin:0 auto; padding:28px 32px 40px; font-size:15px; line-height:1.65; color:var(--text); overflow-wrap:anywhere; }
|
||||||
|
.dv-doc p { margin:0 0 .7em; }
|
||||||
|
.dv-doc h1, .dv-doc h2, .dv-doc h3, .dv-doc h4, .dv-doc h5, .dv-doc h6 { margin:1.2em 0 .5em; line-height:1.3; }
|
||||||
|
.dv-doc h1 { font-size:26px; } .dv-doc h2 { font-size:21px; } .dv-doc h3 { font-size:18px; } .dv-doc h4, .dv-doc h5, .dv-doc h6 { font-size:16px; }
|
||||||
|
.dv-doc ul { margin:0 0 .7em; padding-left:1.4em; }
|
||||||
|
.dv-doc li p { margin:0; }
|
||||||
|
.dv-doc .dv-link { color:var(--accent); }
|
||||||
|
.dv-table { border-collapse:collapse; margin:.4em 0 1em; width:auto; }
|
||||||
|
.dv-table td { border:1px solid var(--border); padding:6px 10px; vertical-align:top; font-size:14px; }
|
||||||
|
.dv-table td p { margin:0; }
|
||||||
|
.dv-scroll { overflow:auto; }
|
||||||
|
.dv-grid { max-height:100%; }
|
||||||
|
.dv-sheet { width:auto; border-collapse:separate; border-spacing:0; font-size:13px; font-variant-numeric:tabular-nums; }
|
||||||
|
.dv-sheet th, .dv-sheet td { border-right:1px solid var(--border); border-bottom:1px solid var(--border); padding:4px 10px;
|
||||||
|
white-space:nowrap; max-width:360px; overflow:hidden; text-overflow:ellipsis; text-align:left; }
|
||||||
|
.dv-sheet thead th { position:sticky; top:0; z-index:1; background:var(--panel); color:var(--muted); font-weight:500; text-align:center; }
|
||||||
|
.dv-sheet tbody th { position:sticky; left:0; background:var(--panel); color:var(--muted); font-weight:500; text-align:right; }
|
||||||
|
.dv-sheet thead th:first-child { left:0; z-index:2; }
|
||||||
|
.dv-sheet td.n { text-align:right; }
|
||||||
|
.dv-tabs { position:sticky; top:0; z-index:3; display:flex; gap:6px; flex-wrap:wrap; padding:8px 12px; background:var(--panel); border-bottom:1px solid var(--border); }
|
||||||
|
.dv-tabs button { height:28px; padding:0 12px; font-size:13px; background:transparent; border:1px solid var(--border); color:var(--muted); }
|
||||||
|
.dv-tabs button.on { border-color:var(--accent); color:var(--text); background:#12202f; }
|
||||||
|
.dv-note { color:var(--muted); font-size:13px; padding:12px 16px; margin:0; }
|
||||||
/* Hex editor: offset, 16 bytes, the same as text. Only the rows in sight are drawn. */
|
/* Hex editor: offset, 16 bytes, the same as text. Only the rows in sight are drawn. */
|
||||||
.hexed { position:relative; height:70vh; overflow:auto; outline:none; background:#0c0f13;
|
.hexed { position:relative; height:70vh; overflow:auto; outline:none; background:#0c0f13;
|
||||||
font:13px/20px ui-monospace,SFMono-Regular,Menlo,monospace; }
|
font:13px/20px ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||||||
@@ -204,12 +228,16 @@
|
|||||||
<div class="fbtns">
|
<div class="fbtns">
|
||||||
<button type="button" class="tg" id="fcase" title="Учитывать регистр">Aa</button>
|
<button type="button" class="tg" id="fcase" title="Учитывать регистр">Aa</button>
|
||||||
<button type="button" class="tg" id="fre" title="Регулярное выражение">.*</button>
|
<button type="button" class="tg" id="fre" title="Регулярное выражение">.*</button>
|
||||||
<button type="button" class="btn ghost" id="fprev" title="Предыдущее (Shift+Enter)">↑</button>
|
<button type="button" class="btn ghost" id="fprev" title="Предыдущее (Shift+Enter)" aria-label="Предыдущее">
|
||||||
<button type="button" class="btn ghost" id="fnext" title="Следующее (Enter)">↓</button>
|
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M6 2.5 11 9.5H1z" fill="currentColor"/></svg>
|
||||||
</div>
|
|
||||||
<button type="button" class="fx" id="fclose" title="Закрыть поиск (Esc)" aria-label="Закрыть поиск">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn ghost" id="fnext" title="Следующее (Enter)" aria-label="Следующее">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M6 9.5 1 2.5h10z" fill="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn ghost fx" id="fclose" title="Закрыть поиск (Esc)" aria-label="Закрыть поиск">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="fin" id="frow2"><input type="text" id="frep" placeholder="Заменить на" spellcheck="false" autocomplete="off"></div>
|
<div class="fin" id="frow2"><input type="text" id="frep" placeholder="Заменить на" spellcheck="false" autocomplete="off"></div>
|
||||||
<div class="fbtns" id="frow2b">
|
<div class="fbtns" id="frow2b">
|
||||||
<button type="button" class="btn ghost" id="frepone">Заменить</button>
|
<button type="button" class="btn ghost" id="frepone">Заменить</button>
|
||||||
@@ -218,6 +246,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="vcode" hidden></div>
|
<div id="vcode" hidden></div>
|
||||||
<div class="vmedia" id="vmedia" hidden></div>
|
<div class="vmedia" id="vmedia" hidden></div>
|
||||||
|
<div class="vdoc" id="vdoc" hidden></div>
|
||||||
<div class="hexed" id="vhexed" tabindex="0" hidden><div id="hxsp"></div><div class="hx-rows" id="hxrows"></div></div>
|
<div class="hexed" id="vhexed" tabindex="0" hidden><div id="hxsp"></div><div class="hx-rows" id="hxrows"></div></div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -233,6 +262,7 @@
|
|||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script src="/static/codemirror/codemirror.js"></script>
|
<script src="/static/codemirror/codemirror.js"></script>
|
||||||
|
<script src="/static/docview.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const agentID = {{.Agent.ID}};
|
const agentID = {{.Agent.ID}};
|
||||||
@@ -331,6 +361,8 @@
|
|||||||
|
|
||||||
const IMG = /\.(png|jpe?g|gif|webp|bmp|ico|avif|svg)$/i, VID = /\.(mp4|m4v|webm|ogv|mov)$/i, AUD = /\.(mp3|wav|ogg|oga|m4a|aac|flac|opus)$/i;
|
const IMG = /\.(png|jpe?g|gif|webp|bmp|ico|avif|svg)$/i, VID = /\.(mp4|m4v|webm|ogv|mov)$/i, AUD = /\.(mp3|wav|ogg|oga|m4a|aac|flac|opus)$/i;
|
||||||
const HEX_CAP = 64 * 1024 * 1024; // what the hex editor takes in memory
|
const HEX_CAP = 64 * 1024 * 1024; // what the hex editor takes in memory
|
||||||
|
const DOC = /\.(pdf|docx?|odt|xlsx|ods|csv|tsv|html?)$/i;
|
||||||
|
const DOC_CAP = 32 * 1024 * 1024; // the biggest document drawn here
|
||||||
|
|
||||||
// Highlighting by the file's name, then its extension, then its #! line.
|
// Highlighting by the file's name, then its extension, then its #! line.
|
||||||
const BY_EXT = {
|
const BY_EXT = {
|
||||||
@@ -382,7 +414,8 @@
|
|||||||
// What the viewer has open now.
|
// What the viewer has open now.
|
||||||
const V = { path: '', kind: '', dirty: false, clean: 0 };
|
const V = { path: '', kind: '', dirty: false, clean: 0 };
|
||||||
function showPane(which) {
|
function showPane(which) {
|
||||||
for (const [id, k] of [['vcode', 'code'], ['vmedia', 'media'], ['vhexed', 'hex']]) $(id).hidden = which !== k;
|
for (const [id, k] of [['vcode', 'code'], ['vmedia', 'media'], ['vhexed', 'hex'], ['vdoc', 'doc']]) $(id).hidden = which !== k;
|
||||||
|
if (which !== 'doc') $('vdoc').innerHTML = '';
|
||||||
if (which !== 'code') closeFind();
|
if (which !== 'code') closeFind();
|
||||||
}
|
}
|
||||||
function setButtons(o) {
|
function setButtons(o) {
|
||||||
@@ -398,6 +431,7 @@
|
|||||||
async function view(e) {
|
async function view(e) {
|
||||||
const path = join(cwd, e.name);
|
const path = join(cwd, e.name);
|
||||||
if (IMG.test(e.name) || VID.test(e.name) || AUD.test(e.name)) return viewMedia(path, e.size);
|
if (IMG.test(e.name) || VID.test(e.name) || AUD.test(e.name)) return viewMedia(path, e.size);
|
||||||
|
if (DOC.test(e.name)) return viewDoc(path, e.size);
|
||||||
const r = await api('read', {path});
|
const r = await api('read', {path});
|
||||||
if (!r.ok) { toast(r.error || 'ошибка', true); return; }
|
if (!r.ok) { toast(r.error || 'ошибка', true); return; }
|
||||||
if (r.data.binary) return viewHex(path, r.data.size);
|
if (r.data.binary) return viewHex(path, r.data.size);
|
||||||
@@ -421,7 +455,6 @@
|
|||||||
V.dirty = false;
|
V.dirty = false;
|
||||||
setButtons({save: editable});
|
setButtons({save: editable});
|
||||||
$('frow2').hidden = $('frow2b').hidden = !editable;
|
$('frow2').hidden = $('frow2b').hidden = !editable;
|
||||||
$('find').classList.toggle('ro', !editable);
|
|
||||||
$('vmsg').textContent = fmtSize(d.size) + ' · ' + modeName(mode) +
|
$('vmsg').textContent = fmtSize(d.size) + ' · ' + modeName(mode) +
|
||||||
(d.truncated ? ' · показаны первые ' + fmtSize(EDIT_LIMIT) : '') + (editable ? '' : ' · только чтение (больше ' + fmtSize(EDIT_LIMIT) + ')') +
|
(d.truncated ? ' · показаны первые ' + fmtSize(EDIT_LIMIT) : '') + (editable ? '' : ' · только чтение (больше ' + fmtSize(EDIT_LIMIT) + ')') +
|
||||||
' · Ctrl/Cmd+F - поиск и замена';
|
' · Ctrl/Cmd+F - поиск и замена';
|
||||||
@@ -444,7 +477,9 @@
|
|||||||
el.onerror = () => { $('vmsg').textContent = fmtSize(size) + ' · браузер не смог это показать'; };
|
el.onerror = () => { $('vmsg').textContent = fmtSize(size) + ' · браузер не смог это показать'; };
|
||||||
el.src = fsURL('download', path) + '&inline=1';
|
el.src = fsURL('download', path) + '&inline=1';
|
||||||
box.appendChild(el);
|
box.appendChild(el);
|
||||||
setButtons({text: /\.svg$/i.test(path), hex: true});
|
// SVG is text: edited as such, not byte by byte.
|
||||||
|
const svg = /\.svg$/i.test(path);
|
||||||
|
setButtons({text: svg, hex: !svg});
|
||||||
$('vmsg').textContent = fmtSize(size);
|
$('vmsg').textContent = fmtSize(size);
|
||||||
$('vtext').onclick = async () => {
|
$('vtext').onclick = async () => {
|
||||||
const r = await api('read', {path});
|
const r = await api('read', {path});
|
||||||
@@ -454,6 +489,70 @@
|
|||||||
};
|
};
|
||||||
$('vhex').onclick = () => { stopMedia(); viewHex(path, size); };
|
$('vhex').onclick = () => { stopMedia(); viewHex(path, size); };
|
||||||
}
|
}
|
||||||
|
// PDF goes to the browser's own viewer; the rest is drawn by docview.js.
|
||||||
|
async function viewDoc(path, size) {
|
||||||
|
openViewer(path);
|
||||||
|
V.kind = 'doc';
|
||||||
|
showPane('doc');
|
||||||
|
const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
// Text with a preview gets the text editor; the rest, the hex one.
|
||||||
|
const isText = ext === 'csv' || ext === 'tsv' || ext === 'html' || ext === 'htm';
|
||||||
|
setButtons({text: isText, hex: !isText});
|
||||||
|
$('vtext').onclick = async () => {
|
||||||
|
const r = await api('read', {path});
|
||||||
|
if (!r.ok) { toast(r.error || 'ошибка', true); return; }
|
||||||
|
viewText(path, r.data);
|
||||||
|
};
|
||||||
|
$('vhex').onclick = () => viewHex(path, size);
|
||||||
|
const box = $('vdoc');
|
||||||
|
$('vmsg').textContent = fmtSize(size);
|
||||||
|
if (ext === 'pdf') {
|
||||||
|
const src = fsURL('download', path) + '&inline=1';
|
||||||
|
box.innerHTML = '<iframe class="dv-pdf" title="PDF"></iframe>';
|
||||||
|
box.firstChild.src = src;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'открыть в новой вкладке';
|
||||||
|
$('vmsg').append(' · ', a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ext === 'html' || ext === 'htm') {
|
||||||
|
// The page as it looks, in a frame that runs none of its scripts.
|
||||||
|
const r = await api('read', {path});
|
||||||
|
if (V.path !== path || V.kind !== 'doc') return;
|
||||||
|
if (!r.ok) { box.innerHTML = '<p class="dv-note">не удалось показать: ' + esc(r.error || 'ошибка') + '</p>'; return; }
|
||||||
|
const f = document.createElement('iframe');
|
||||||
|
f.className = 'dv-html';
|
||||||
|
f.setAttribute('sandbox', '');
|
||||||
|
f.title = 'HTML';
|
||||||
|
f.srcdoc = new TextDecoder().decode(b64ToBytes(r.data.content));
|
||||||
|
box.innerHTML = '';
|
||||||
|
box.appendChild(f);
|
||||||
|
if (r.data.truncated) $('vmsg').textContent += ' · показаны первые ' + fmtSize(EDIT_LIMIT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (size > DOC_CAP) { box.innerHTML = '<p class="dv-note">файл больше ' + fmtSize(DOC_CAP) + ' - для просмотра скачайте его</p>'; return; }
|
||||||
|
box.innerHTML = '<p class="dv-note">загрузка…</p>';
|
||||||
|
let html;
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
const r = await fetch(fsURL('download', path));
|
||||||
|
if (!r.ok) throw new Error((await r.text()).trim() || ('HTTP ' + r.status));
|
||||||
|
const buf = await r.arrayBuffer();
|
||||||
|
if (V.path !== path || V.kind !== 'doc') return;
|
||||||
|
html = ext === 'docx' ? await DocView.docx(buf)
|
||||||
|
: ext === 'odt' ? await DocView.odt(buf)
|
||||||
|
: ext === 'xlsx' ? await DocView.xlsx(buf)
|
||||||
|
: ext === 'ods' ? await DocView.ods(buf)
|
||||||
|
: ext === 'doc' ? DocView.doc(buf)
|
||||||
|
: DocView.csv(new TextDecoder().decode(buf), ext === 'tsv');
|
||||||
|
} catch (err) {
|
||||||
|
html = '<p class="dv-note">не удалось показать: ' + esc(err.message) + '</p>';
|
||||||
|
} finally { setBusy(false); }
|
||||||
|
if (V.path !== path || V.kind !== 'doc') return;
|
||||||
|
box.innerHTML = html;
|
||||||
|
DocView.bindTabs(box);
|
||||||
|
}
|
||||||
|
|
||||||
function stopMedia() {
|
function stopMedia() {
|
||||||
const el = $('vmedia').firstChild;
|
const el = $('vmedia').firstChild;
|
||||||
if (el && el.pause) el.pause();
|
if (el && el.pause) el.pause();
|
||||||
|
|||||||
Reference in new issue
Block a user