WebOrbiton
v1.3.0

AtheraStudio SyncDrop

829 lines · 36.3 KB
  1. (function () {
  2. 'use strict';
  3. ​
  4. var body = document.body;
  5. var CSRF = body.getAttribute('data-csrf') || '';
  6. var BASE = body.getAttribute('data-base') || '';
  7. var STR = {};
  8. try { STR = JSON.parse(body.getAttribute('data-i18n') || '{}'); } catch (e) {}
  9. var enc = new TextEncoder();
  10. ​
  11. function t(key, vars) {
  12. var s = STR[key] || key;
  13. if (vars) Object.keys(vars).forEach(function (k) { s = s.split('{' + k + '}').join(String(vars[k])); });
  14. return s;
  15. }
  16. ​
  17. function toast(msg, tone) {
  18. if (window.Orbitarium && window.Orbitarium.toast) window.Orbitarium.toast(msg, { tone: tone || 'primary' });
  19. }
  20. ​
  21. function bytes(n) {
  22. var u = ['B', 'KB', 'MB', 'GB'];
  23. var i = 0;
  24. while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
  25. return (i === 0 ? n : n.toFixed(1)) + ' ' + u[i];
  26. }
  27. ​
  28. function hex(buf) {
  29. var b = new Uint8Array(buf);
  30. var out = '';
  31. for (var i = 0; i < b.length; i++) out += (b[i] < 16 ? '0' : '') + b[i].toString(16);
  32. return out;
  33. }
  34. ​
  35. function zeroBits(buf, bits) {
  36. var b = new Uint8Array(buf);
  37. var i = 0;
  38. while (bits >= 8) { if (b[i++] !== 0) return false; bits -= 8; }
  39. return bits === 0 || (b[i] >> (8 - bits)) === 0;
  40. }
  41. ​
  42. function powBits(challenge) {
  43. try {
  44. var p = challenge.split('.')[0].replace(/-/g, '+').replace(/_/g, '/');
  45. while (p.length % 4) p += '=';
  46. return Math.max(0, Math.min(32, Number(JSON.parse(atob(p)).b) || 0));
  47. } catch (e) { return 0; }
  48. }
  49. ​
  50. async function solve(challenge) {
  51. var bits = powBits(challenge);
  52. var prefix = challenge + ':';
  53. var batch = 512;
  54. for (var start = 0; start < 4e9; start += batch) {
  55. var jobs = [];
  56. for (var k = 0; k < batch; k++) jobs.push(crypto.subtle.digest('SHA-256', enc.encode(prefix + (start + k))));
  57. var digests = await Promise.all(jobs);
  58. for (var j = 0; j < batch; j++) if (zeroBits(digests[j], bits)) return start + j;
  59. }
  60. return 0;
  61. }
  62. ​
  63. function initPow() {
  64. document.querySelectorAll('form[data-pow-form]').forEach(function (form) {
  65. var solved = false;
  66. var started = null;
  67. var challenge = form.querySelector('input[name="_pow"]');
  68. var nonce = form.querySelector('input[name="_nonce"]');
  69. if (!challenge || !nonce || !window.crypto || !crypto.subtle) return;
  70. var begin = function () {
  71. if (!started) started = solve(challenge.value).then(function (n) { nonce.value = String(n); solved = true; });
  72. return started;
  73. };
  74. setTimeout(begin, 400);
  75. form.addEventListener('submit', function (e) {
  76. if (solved) return;
  77. e.preventDefault();
  78. var btn = form.querySelector('button[type="submit"]');
  79. if (btn) { btn.classList.add('is-loading'); btn.setAttribute('aria-label', t('js_solving')); }
  80. begin().then(function () { form.submit(); });
  81. });
  82. });
  83. }
  84. ​
  85. function initConfirm() {
  86. document.addEventListener('submit', function (e) {
  87. var form = e.target;
  88. if (form.matches && form.matches('form[data-confirm]') && !window.confirm(form.getAttribute('data-confirm'))) e.preventDefault();
  89. }, true);
  90. document.addEventListener('click', function (e) {
  91. var btn = e.target.closest && e.target.closest('button[data-confirm]');
  92. if (btn && !window.confirm(btn.getAttribute('data-confirm'))) e.preventDefault();
  93. }, true);
  94. document.addEventListener('submit', function (e) {
  95. var form = e.target;
  96. if (e.defaultPrevented || !form.matches || !form.matches('form[data-busy]')) return;
  97. if (form.getAttribute('data-sent')) { e.preventDefault(); return; }
  98. form.setAttribute('data-sent', '1');
  99. var btn = form.querySelector('button[type="submit"]');
  100. if (btn) { btn.classList.add('is-loading'); btn.setAttribute('aria-busy', 'true'); }
  101. document.querySelectorAll('form[data-busy] button[type="submit"]').forEach(function (b) { if (b !== btn) b.disabled = true; });
  102. });
  103. }
  104. ​
  105. function copyText(text) {
  106. var done = function () { toast(t('js_copied'), 'success'); };
  107. if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, function () {});
  108. }
  109. ​
  110. function initCopy() {
  111. document.addEventListener('click', function (e) {
  112. var b = e.target.closest && e.target.closest('[data-copy-btn]');
  113. if (b) { var src = b.closest('[data-copy]'); if (src) copyText(src.getAttribute('data-copy')); return; }
  114. var c = e.target.closest && e.target.closest('[data-codes-copy]');
  115. var list = document.querySelector('[data-codes]');
  116. if (c && list) { copyText(list.getAttribute('data-codes')); return; }
  117. var d = e.target.closest && e.target.closest('[data-codes-download]');
  118. if (d && list) {
  119. var blob = new Blob(['AtheraStudio SyncDrop\n\n' + list.getAttribute('data-codes') + '\n'], { type: 'text/plain' });
  120. var a = document.createElement('a');
  121. a.href = URL.createObjectURL(blob);
  122. a.download = d.getAttribute('data-codes-download');
  123. document.body.appendChild(a);
  124. a.click();
  125. setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 500);
  126. }
  127. });
  128. }
  129. ​
  130. function initTimes() {
  131. document.querySelectorAll('time.sd-time[datetime]').forEach(function (el) {
  132. var d = new Date(el.getAttribute('datetime'));
  133. if (!isNaN(d)) el.textContent = d.toLocaleString(document.documentElement.lang || undefined, { dateStyle: 'medium', timeStyle: 'short' });
  134. });
  135. }
  136. ​
  137. function api(action, payload) {
  138. return fetch(BASE + '?api=' + action, {
  139. method: 'POST',
  140. credentials: 'same-origin',
  141. headers: { 'Content-Type': 'application/json', 'X-SyncDrop-CSRF': CSRF },
  142. body: JSON.stringify(payload)
  143. }).then(function (res) {
  144. return res.json().catch(function () { return {}; }).then(function (data) {
  145. data._status = res.status;
  146. return data;
  147. });
  148. });
  149. }
  150. ​
  151. function errorText(data) {
  152. if (data._status === 401 || data.error === 'auth' || data.error === 'csrf') return t('js_err_auth');
  153. if (data.error === 'busy') return t('js_err_busy');
  154. if (data.error === 'disk') return t('js_err_disk');
  155. if (data.error === 'verify') return t('js_err_verify');
  156. if (data.rolled_back) return t('js_rolled_back');
  157. return t('js_err_generic');
  158. }
  159. ​
  160. function readEntries(reader) {
  161. return new Promise(function (resolve) {
  162. var all = [];
  163. var next = function () {
  164. reader.readEntries(function (batch) {
  165. if (!batch.length) return resolve(all);
  166. all = all.concat(Array.prototype.slice.call(batch));
  167. next();
  168. }, function () { resolve(all); });
  169. };
  170. next();
  171. });
  172. }
  173. ​
  174. async function walk(entry, out) {
  175. if (entry.isFile) {
  176. var file = await new Promise(function (resolve) { entry.file(resolve, function () { resolve(null); }); });
  177. if (file) out.push({ file: file, path: entry.fullPath.replace(/^\/+/, '') });
  178. } else if (entry.isDirectory) {
  179. var children = await readEntries(entry.createReader());
  180. for (var i = 0; i < children.length; i++) await walk(children[i], out);
  181. }
  182. }
  183. ​
  184. var VIEW_MAX = 1048576;
  185. var HL_MAX = 600000;
  186. ​
  187. function esc(s) {
  188. return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  189. }
  190. ​
  191. var RX = {
  192. block: /\/\*[\s\S]*?(?:\*\/|(?![\s\S]))/.source,
  193. line: /\/\/[^\n]*/.source,
  194. hash: /#[^\n]*/.source,
  195. dq: /"(?:[^"\\\n]|\\.)*"?/.source,
  196. sq: /'(?:[^'\\\n]|\\.)*'?/.source,
  197. tpl: /`(?:[^`\\]|\\[\s\S])*`?/.source,
  198. num: /\b(?:0[xX][\da-fA-F_]+|\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/.source,
  199. fn: /\b[A-Za-z_$][\w$]*(?=\s*\()/.source
  200. };
  201. ​
  202. function kw(words) {
  203. return '\\b(?:' + words.split(' ').join('|') + ')\\b';
  204. }
  205. ​
  206. function tagTokens(text) {
  207. var out = [];
  208. var head = /^<[!?\/]?[\w:.-]*/.exec(text)[0];
  209. out.push({ c: 't', t: head });
  210. var re = /(\s+)|([^\s=>"'\/]+)(?=\s*=)|("[^"]*"?|'[^']*'?)|(\/?\??>)|([\s\S])/g;
  211. var m;
  212. var rest = text.slice(head.length);
  213. while ((m = re.exec(rest))) {
  214. if (m[2]) out.push({ c: 'a', t: m[2] });
  215. else if (m[3]) out.push({ c: 's', t: m[3] });
  216. else if (m[4]) out.push({ c: 't', t: m[4] });
  217. else out.push({ c: '', t: m[0] });
  218. }
  219. return out;
  220. }
  221. ​
  222. var JS_KW = 'abstract as async await break case catch class const continue debugger declare default delete do else enum export extends false finally for from function get if implements import in instanceof interface let new null of package private protected public readonly return set static super switch this throw true try type typeof undefined var void while with yield';
  223. var C_KW = 'auto bool boolean break byte case catch char class const continue default delete do double else enum extends false final finally float fn for func go if impl import in int interface let long loop match mod mut namespace new nil null override package private protected pub public return self short static string struct super switch this throw true try type typedef unsigned use using var void volatile when where while';
  224. var PHP_KW = 'abstract and array as break callable case catch class clone const continue declare default do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile enum exit extends false final finally fn for foreach function global goto if implements include include_once instanceof insteadof interface isset list match namespace new null or print private protected public readonly require require_once return static switch throw trait true try unset use var while xor yield never mixed void int float bool string object iterable self parent';
  225. var PY_KW = 'and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield self match case';
  226. var SH_KW = 'if then else elif fi for while until do done case esac in function return local export readonly unset shift exit echo source alias set';
  227. var SQL_KW = 'add all alter and as asc auto_increment between by cascade case check column constraint create database default delete desc distinct drop else end exists foreign from group having if in index inner insert into is join key left like limit not null on or order outer primary references right select set table then truncate union unique update values view when where with int integer varchar text char date datetime timestamp boolean decimal float double bigint';
  228. ​
  229. var SPECS = {
  230. js: { f: 'gm', r: [[RX.block, 'c'], [RX.line, 'c'], [RX.tpl, 's'], [RX.dq, 's'], [RX.sq, 's'], [kw(JS_KW), 'k'], [RX.num, 'n'], [RX.fn, 'f']] },
  231. c: { f: 'gm', r: [[RX.block, 'c'], [RX.line, 'c'], [RX.dq, 's'], [RX.sq, 's'], [/#\s*\w+/.source, 't'], [kw(C_KW), 'k'], [RX.num, 'n'], [RX.fn, 'f']] },
  232. php: { f: 'gm', r: [[RX.block, 'c'], [RX.line, 'c'], [/#(?!\[)[^\n]*/.source, 'c'], [/<\?(?:php|=)?|\?>/.source, 't'], [RX.dq, 's'], [RX.sq, 's'], [/\$[A-Za-z_]\w*/.source, 'v'], [kw(PHP_KW), 'k'], [RX.num, 'n'], [RX.fn, 'f']] },
  233. css: { f: 'gm', r: [[RX.block, 'c'], [RX.dq, 's'], [RX.sq, 's'], [/@[\w-]+/.source, 'k'], [/#[\da-fA-F]{3,8}\b/.source, 'n'], [/(?<=[{;]\s*)-{0,2}[A-Za-z][\w-]*(?=\s*:)/.source, 'p'], [/\b\d*\.?\d+(?:px|r?em|%|vh|vw|dvh|svh|ch|ex|s|ms|deg|fr|pt)?\b/.source, 'n'], [/![a-z]+/.source, 'k'], [RX.fn, 'f']] },
  234. html: { f: 'gm', r: [[/<!--[\s\S]*?(?:-->|(?![\s\S]))/.source, 'c'], [/<[!?\/]?[A-Za-z][^<>]*>?/.source, 't', tagTokens], [/&#?\w+;/.source, 'n']] },
  235. json: { f: 'gm', r: [[/"(?:[^"\\\n]|\\.)*"(?=\s*:)/.source, 'p'], [RX.dq, 's'], [kw('true false null'), 'k'], [/-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/.source, 'n']] },
  236. py: { f: 'gm', r: [[RX.hash, 'c'], [/[rbuRBU]{0,2}(?:"""[\s\S]*?(?:"""|(?![\s\S]))|'''[\s\S]*?(?:'''|(?![\s\S])))/.source, 's'], [RX.dq, 's'], [RX.sq, 's'], [/@[\w.]+/.source, 'v'], [kw(PY_KW), 'k'], [RX.num, 'n'], [RX.fn, 'f']] },
  237. sh: { f: 'gm', r: [[/(?<![\w$])#[^\n]*/.source, 'c'], [RX.dq, 's'], [RX.sq, 's'], [/\$\{?[A-Za-z_]\w*\}?/.source, 'v'], [kw(SH_KW), 'k'], [RX.num, 'n']] },
  238. yaml: { f: 'gm', r: [[/(?<!\S)#[^\n]*/.source, 'c'], [RX.dq, 's'], [RX.sq, 's'], [/^[ \t-]*[\w.\/-]+(?=\s*:(?:\s|$))/.source, 'p'], [kw('true false null yes no on off'), 'k'], [RX.num, 'n']] },
  239. ini: { f: 'gm', r: [[/^\s*[;#][^\n]*/.source, 'c'], [/^\s*\[[^\]\n]+\]/.source, 'k'], [/^\s*[\w.-]+(?=\s*=)/.source, 'p'], [RX.dq, 's'], [RX.sq, 's'], [RX.num, 'n']] },
  240. apache: { f: 'gm', r: [[/^\s*#[^\n]*/.source, 'c'], [/<\/?[A-Za-z]+[^>\n]*>/.source, 't'], [/^\s*[A-Za-z][\w]*/.source, 'k'], [RX.dq, 's'], [/%\{[\w:]+\}|\$\d/.source, 'v'], [RX.num, 'n']] },
  241. sql: { f: 'gmi', r: [[/--[^\n]*/.source, 'c'], [RX.block, 'c'], [RX.sq, 's'], [RX.dq, 's'], [/`[^`\n]*`/.source, 'p'], [kw(SQL_KW), 'k'], [RX.num, 'n']] },
  242. md: { f: 'gm', r: [[/^#{1,6} [^\n]*/.source, 'k'], [/```[\s\S]*?(?:```|(?![\s\S]))/.source, 's'], [/`[^`\n]+`/.source, 's'], [/\*\*[^*\n]+\*\*/.source, 'v'], [/\[[^\]\n]+\]\([^)\n]+\)/.source, 'f'], [/^\s*(?:[-*+]|\d+\.)\s/.source, 'n']] }
  243. };
  244. ​
  245. var EXT = {
  246. js: 'js', mjs: 'js', cjs: 'js', jsx: 'js', ts: 'js', tsx: 'js', mts: 'js', cts: 'js',
  247. json: 'json', webmanifest: 'json', map: 'json', jsonc: 'json',
  248. php: 'php', phtml: 'php', inc: 'php',
  249. css: 'css', scss: 'css', less: 'css', sass: 'css',
  250. html: 'html', htm: 'html', xhtml: 'html', xml: 'html', svg: 'html', vue: 'html', svelte: 'html', twig: 'html', blade: 'html', tpl: 'html',
  251. py: 'py', sh: 'sh', bash: 'sh', zsh: 'sh', yml: 'yaml', yaml: 'yaml', sql: 'sql', md: 'md', markdown: 'md',
  252. ini: 'ini', conf: 'ini', cfg: 'ini', toml: 'ini', properties: 'ini',
  253. c: 'c', h: 'c', cpp: 'c', hpp: 'c', cc: 'c', cs: 'c', java: 'c', go: 'c', rs: 'c', kt: 'c', swift: 'c', dart: 'c', scala: 'c'
  254. };
  255. ​
  256. function langOf(path) {
  257. var name = path.split('/').pop().toLowerCase();
  258. if (name === '.htaccess' || name === 'httpd.conf' || name === '.user.ini') return name === '.user.ini' ? 'ini' : 'apache';
  259. if (/\.blade\.php$/.test(name)) return 'html';
  260. var dot = name.lastIndexOf('.');
  261. return dot >= 0 ? EXT[name.slice(dot + 1)] || '' : '';
  262. }
  263. ​
  264. function highlight(text, lang) {
  265. var spec = SPECS[lang];
  266. var tokens = [];
  267. if (!spec || text.length > HL_MAX) {
  268. tokens.push({ c: '', t: text });
  269. } else {
  270. if (!spec.re) spec.re = new RegExp(spec.r.map(function (r) { return '(' + r[0] + ')'; }).join('|'), spec.f);
  271. var re = spec.re;
  272. re.lastIndex = 0;
  273. var last = 0;
  274. var m;
  275. while ((m = re.exec(text))) {
  276. if (m[0] === '') { re.lastIndex++; continue; }
  277. if (m.index > last) tokens.push({ c: '', t: text.slice(last, m.index) });
  278. var g = 1;
  279. while (g < m.length && m[g] === undefined) g++;
  280. var rule = spec.r[g - 1];
  281. if (rule[2]) Array.prototype.push.apply(tokens, rule[2](m[0]));
  282. else tokens.push({ c: rule[1], t: m[0] });
  283. last = re.lastIndex;
  284. }
  285. if (last < text.length) tokens.push({ c: '', t: text.slice(last) });
  286. }
  287. var lines = [''];
  288. tokens.forEach(function (tok) {
  289. var parts = tok.t.split('\n');
  290. for (var i = 0; i < parts.length; i++) {
  291. if (i > 0) lines.push('');
  292. if (parts[i] === '') continue;
  293. lines[lines.length - 1] += tok.c ? '<span class="tk-' + tok.c + '">' + esc(parts[i]) + '</span>' : esc(parts[i]);
  294. }
  295. });
  296. return lines;
  297. }
  298. ​
  299. function prepare(text, lang) {
  300. var norm = text.replace(/^/, '').replace(/\r\n?/g, '\n');
  301. if (norm.slice(-1) === '\n') norm = norm.slice(0, -1);
  302. return { raw: norm.split('\n'), html: highlight(norm, lang) };
  303. }
  304. ​
  305. function myers(A, B, maxD) {
  306. var n = A.length;
  307. var m = B.length;
  308. var max = n + m;
  309. if (max === 0) return [];
  310. var off = max + 1;
  311. var size = 2 * max + 3;
  312. var v = new Int32Array(size);
  313. var trace = [];
  314. for (var d = 0; d <= max; d++) {
  315. if (d > maxD) return null;
  316. var lo = Math.max(0, off - d - 1);
  317. trace.push({ lo: lo, v: v.slice(lo, Math.min(size, off + d + 2)) });
  318. for (var k = -d; k <= d; k += 2) {
  319. var x = (k === -d || (k !== d && v[off + k - 1] < v[off + k + 1])) ? v[off + k + 1] : v[off + k - 1] + 1;
  320. var y = x - k;
  321. while (x < n && y < m && A[x] === B[y]) { x++; y++; }
  322. v[off + k] = x;
  323. if (x >= n && y >= m) return backtrack(trace, off, x, y);
  324. }
  325. }
  326. return null;
  327. }
  328. ​
  329. function backtrack(trace, off, x, y) {
  330. var ops = [];
  331. for (var d = trace.length - 1; d >= 0; d--) {
  332. var snap = trace[d];
  333. var get = function (k) { return snap.v[off + k - snap.lo]; };
  334. var k = x - y;
  335. var pk = (k === -d || (k !== d && get(k - 1) < get(k + 1))) ? k + 1 : k - 1;
  336. var px = get(pk);
  337. var py = px - pk;
  338. while (x > px && y > py) { ops.push(['=', x - 1, y - 1]); x--; y--; }
  339. if (d > 0) {
  340. if (x === px) ops.push(['+', -1, y - 1]);
  341. else ops.push(['-', x - 1, -1]);
  342. }
  343. x = px;
  344. y = py;
  345. }
  346. return ops.reverse();
  347. }
  348. ​
  349. function diffLines(a, b) {
  350. var start = 0;
  351. while (start < a.length && start < b.length && a[start] === b[start]) start++;
  352. var endA = a.length;
  353. var endB = b.length;
  354. while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }
  355. var ids = new Map();
  356. var id = function (s) { var v = ids.get(s); if (v === undefined) { v = ids.size; ids.set(s, v); } return v; };
  357. var ops = myers(a.slice(start, endA).map(id), b.slice(start, endB).map(id), 2000);
  358. if (!ops) return null;
  359. var out = [];
  360. for (var i = 0; i < start; i++) out.push(['=', i, i]);
  361. ops.forEach(function (op) { out.push([op[0], op[1] < 0 ? -1 : op[1] + start, op[2] < 0 ? -1 : op[2] + start]); });
  362. for (var j = 0; endA + j < a.length; j++) out.push(['=', endA + j, endB + j]);
  363. return out;
  364. }
  365. ​
  366. function codeRows(html) {
  367. var out = new Array(html.length);
  368. for (var i = 0; i < html.length; i++) out[i] = '<div class="sd-row"><span class="sd-ln" data-l="' + (i + 1) + '"></span><span class="sd-lc">' + html[i] + '</span></div>';
  369. return '<div class="sd-code sd-code-plain">' + out.join('') + '</div>';
  370. }
  371. ​
  372. function diffRows(ops, oldDoc, newDoc) {
  373. var CONTEXT = 3;
  374. var keep = new Uint8Array(ops.length);
  375. for (var i = 0; i < ops.length; i++) {
  376. if (ops[i][0] === '=') continue;
  377. for (var j = Math.max(0, i - CONTEXT); j <= Math.min(ops.length - 1, i + CONTEXT); j++) keep[j] = 1;
  378. }
  379. var out = [];
  380. var skipped = 0;
  381. var flush = function () {
  382. if (skipped) out.push('<div class="sd-gap">' + esc(t('js_view_hidden', { n: skipped })) + '</div>');
  383. skipped = 0;
  384. };
  385. for (var k = 0; k < ops.length; k++) {
  386. var op = ops[k];
  387. if (!keep[k]) { skipped++; continue; }
  388. flush();
  389. if (op[0] === '=') out.push('<div class="sd-row"><span class="sd-ln" data-l="' + (op[1] + 1) + '"></span><span class="sd-ln" data-l="' + (op[2] + 1) + '"></span><span class="sd-mk"></span><span class="sd-lc">' + newDoc.html[op[2]] + '</span></div>');
  390. else if (op[0] === '-') out.push('<div class="sd-row sd-del"><span class="sd-ln" data-l="' + (op[1] + 1) + '"></span><span class="sd-ln"></span><span class="sd-mk">-</span><span class="sd-lc">' + oldDoc.html[op[1]] + '</span></div>');
  391. else out.push('<div class="sd-row sd-add"><span class="sd-ln"></span><span class="sd-ln" data-l="' + (op[2] + 1) + '"></span><span class="sd-mk">+</span><span class="sd-lc">' + newDoc.html[op[2]] + '</span></div>');
  392. }
  393. flush();
  394. return '<div class="sd-code">' + out.join('') + '</div>';
  395. }
  396. ​
  397. async function readLocal(file) {
  398. if (file.size > VIEW_MAX) return { err: 'large' };
  399. var bytesArr = new Uint8Array(await file.arrayBuffer());
  400. var probe = Math.min(bytesArr.length, 8192);
  401. for (var i = 0; i < probe; i++) if (bytesArr[i] === 0) return { err: 'binary' };
  402. try {
  403. return { text: new TextDecoder('utf-8', { fatal: true }).decode(bytesArr) };
  404. } catch (e) {
  405. return { err: 'binary' };
  406. }
  407. }
  408. ​
  409. function initViewer(getTarget) {
  410. var modal = document.getElementById('sd-viewer');
  411. if (!modal) return null;
  412. var q = function (sel) { return modal.querySelector(sel); };
  413. var title = q('[data-viewer-title]');
  414. var meta = q('[data-viewer-meta]');
  415. var tabs = q('[data-viewer-tabs]');
  416. var stat = q('[data-viewer-stat]');
  417. var wrap = q('[data-viewer-wrap]');
  418. var copyBtn = q('[data-viewer-copy]');
  419. var body = q('[data-viewer-body]');
  420. var current = null;
  421. var token = 0;
  422. ​
  423. try { wrap.checked = localStorage.getItem('sd-wrap') === '1'; } catch (e) {}
  424. body.classList.toggle('is-wrap', wrap.checked);
  425. wrap.addEventListener('change', function () {
  426. body.classList.toggle('is-wrap', wrap.checked);
  427. try { localStorage.setItem('sd-wrap', wrap.checked ? '1' : '0'); } catch (e) {}
  428. });
  429. ​
  430. function note(text) {
  431. body.textContent = '';
  432. var p = document.createElement('p');
  433. p.className = 'sd-viewer-note';
  434. p.textContent = text;
  435. body.appendChild(p);
  436. }
  437. ​
  438. function setStat(add, del) {
  439. stat.textContent = '';
  440. if (add === null) return;
  441. var a = document.createElement('span'); a.className = 'sd-plus'; a.textContent = '+' + add;
  442. var d = document.createElement('span'); d.className = 'sd-minus'; d.textContent = ' −' + del;
  443. stat.appendChild(a); stat.appendChild(d);
  444. }
  445. ​
  446. function doc(which) {
  447. if (!current.docs[which]) current.docs[which] = prepare(which === 'old' ? current.server.text : current.local.text, current.lang);
  448. return current.docs[which];
  449. }
  450. ​
  451. function show(view) {
  452. Array.prototype.forEach.call(tabs.querySelectorAll('[data-view]'), function (b) { b.setAttribute('aria-selected', b.getAttribute('data-view') === view ? 'true' : 'false'); });
  453. copyBtn.hidden = view === 'diff';
  454. current.view = view;
  455. setStat(null);
  456. body.scrollTop = 0;
  457. body.scrollLeft = 0;
  458. if (view === 'diff') {
  459. var oldDoc = doc('old');
  460. var newDoc = doc('new');
  461. var ops = current.ops !== undefined ? current.ops : (current.ops = diffLines(oldDoc.raw, newDoc.raw));
  462. if (!ops) { note(t('js_view_too_many')); return; }
  463. var add = 0;
  464. var del = 0;
  465. ops.forEach(function (op) { if (op[0] === '+') add++; else if (op[0] === '-') del++; });
  466. setStat(add, del);
  467. if (!add && !del) { note(t('js_view_same')); return; }
  468. body.innerHTML = diffRows(ops, oldDoc, newDoc);
  469. return;
  470. }
  471. var d = doc(view);
  472. meta.textContent = current.metaBase + ' · ' + t('js_view_lines', { n: d.raw.length });
  473. body.innerHTML = codeRows(d.html);
  474. }
  475. ​
  476. tabs.addEventListener('click', function (e) {
  477. var b = e.target.closest('[data-view]');
  478. if (b && current) show(b.getAttribute('data-view'));
  479. });
  480. ​
  481. copyBtn.addEventListener('click', function () {
  482. if (!current || !navigator.clipboard) return;
  483. var text = current.view === 'old' ? current.server.text : current.local.text;
  484. navigator.clipboard.writeText(text).then(function () { toast(t('js_copied'), 'success'); });
  485. });
  486. ​
  487. async function open(it) {
  488. var my = ++token;
  489. current = null;
  490. title.textContent = it.path;
  491. var metaBase = bytes(it.size) + ' · ' + t('js_status_' + it.st);
  492. meta.textContent = metaBase;
  493. tabs.hidden = true;
  494. copyBtn.hidden = true;
  495. setStat(null);
  496. note(t('js_view_loading'));
  497. if (window.Orbitarium && window.Orbitarium.modal) window.Orbitarium.modal.open('sd-viewer');
  498. try {
  499. var local = await readLocal(it.file);
  500. if (my !== token) return;
  501. if (local.err) { note(t(local.err === 'large' ? 'js_view_large' : 'js_view_binary')); return; }
  502. var server = null;
  503. if (it.st === 'c') {
  504. server = await api('view', { target: getTarget(), path: it.path });
  505. if (my !== token) return;
  506. if (server._status !== 200) throw server;
  507. }
  508. current = { it: it, lang: langOf(it.path), local: local, server: server, docs: {}, metaBase: metaBase };
  509. var both = server && typeof server.text === 'string';
  510. tabs.hidden = !both;
  511. if (both) { show('diff'); return; }
  512. show('new');
  513. if (server && !both) {
  514. var info = document.createElement('p');
  515. info.className = 'sd-viewer-note';
  516. info.textContent = t(server.binary ? 'js_view_server_binary' : server.large ? 'js_view_server_large' : 'js_view_server_missing');
  517. body.insertBefore(info, body.firstChild);
  518. }
  519. } catch (err) {
  520. if (my === token) note(errorText(err || {}));
  521. }
  522. }
  523. ​
  524. return { open: open };
  525. }
  526. ​
  527. function initSync() {
  528. var root = document.querySelector('[data-sync]');
  529. if (!root) return;
  530. var CHUNK = Number(root.getAttribute('data-chunk')) || 1048576;
  531. var BATCH = Number(root.getAttribute('data-batch')) || 1000;
  532. var MAX_FILES = Number(root.getAttribute('data-max-files')) || 20000;
  533. var MAX_SIZE = Number(root.getAttribute('data-max-size')) || 268435456;
  534. var $ = function (sel) { return root.querySelector(sel); };
  535. var targetSel = document.getElementById('sd-target');
  536. var picker = $('[data-picker]');
  537. var drop = $('[data-drop]');
  538. var strip = $('[data-strip]');
  539. var sendBtn = $('[data-send]');
  540. var sendLabel = $('[data-send-label]');
  541. var progress = $('[data-progress]');
  542. var phase = $('[data-phase]');
  543. var percent = $('[data-percent]');
  544. var bar = $('[data-bar]');
  545. var result = $('[data-result]');
  546. var listCard = $('[data-list-card]');
  547. var list = $('[data-list]');
  548. var listCount = $('[data-list-count]');
  549. var items = [];
  550. var busy = false;
  551. var ALWAYS = /(^|\/)(\.git|\.svn|\.hg|\.syncdrop[^/]*)(\/|$)|(^|\/)\.env(\.[^/]*)?$|\.env$/i;
  552. var viewer = initViewer(function () { return targetSel.value; });
  553. var viewIcon = document.querySelector('template[data-view-icon]');
  554. ​
  555. function setBusy(on) {
  556. busy = on;
  557. drop.classList.toggle('is-busy', on);
  558. targetSel.disabled = on;
  559. }
  560. ​
  561. function showProgress(label, done, total) {
  562. progress.hidden = false;
  563. phase.textContent = label;
  564. var p = total > 0 ? Math.min(100, Math.round(done / total * 100)) : 0;
  565. percent.textContent = p + '%';
  566. bar.style.setProperty('--orb-value', p + '%');
  567. }
  568. ​
  569. function message(kind, text) {
  570. result.textContent = '';
  571. var box = document.createElement('div');
  572. box.className = 'orb-alert orb-alert-' + kind;
  573. var span = document.createElement('div');
  574. span.className = 'orb-alert-text';
  575. span.textContent = text;
  576. box.appendChild(span);
  577. result.appendChild(box);
  578. }
  579. ​
  580. function counts() {
  581. var c = { n: 0, c: 0, s: 0, x: 0 };
  582. items.forEach(function (it) {
  583. if (it.st === 'n') c.n++;
  584. else if (it.st === 'c') c.c++;
  585. else if (it.st === 's') c.s++;
  586. else c.x++;
  587. });
  588. Object.keys(c).forEach(function (k) { var el = root.querySelector('[data-stat="' + k + '"]'); if (el) el.textContent = c[k]; });
  589. return c;
  590. }
  591. ​
  592. function pending() {
  593. return items.filter(function (it) { return it.st === 'n' || it.st === 'c'; });
  594. }
  595. ​
  596. function renderList() {
  597. var todo = pending();
  598. list.textContent = '';
  599. listCard.hidden = todo.length === 0;
  600. listCount.textContent = t('js_files', { n: todo.length });
  601. todo.slice(0, 500).forEach(function (it) {
  602. var tr = document.createElement('tr');
  603. var a = document.createElement('td'); a.setAttribute('data-label', ''); a.className = 'orb-mono sd-path'; a.textContent = it.path;
  604. var b = document.createElement('td'); b.textContent = bytes(it.size);
  605. var c = document.createElement('td');
  606. var pill = document.createElement('span');
  607. pill.className = 'orb-pill ' + (it.st === 'n' ? 'orb-pill-success' : 'orb-pill-warning');
  608. pill.textContent = t('js_status_' + it.st);
  609. c.appendChild(pill);
  610. var v = document.createElement('td');
  611. v.className = 'sd-view-cell';
  612. if (viewer) {
  613. var vb = document.createElement('button');
  614. vb.type = 'button';
  615. vb.className = 'orb-btn orb-btn-ghost orb-btn-xs';
  616. if (viewIcon) vb.appendChild(viewIcon.content.cloneNode(true));
  617. var vl = document.createElement('span');
  618. vl.textContent = t('js_view');
  619. vb.appendChild(vl);
  620. vb.addEventListener('click', function () { viewer.open(it); });
  621. v.appendChild(vb);
  622. }
  623. tr.appendChild(a); tr.appendChild(b); tr.appendChild(c); tr.appendChild(v);
  624. list.appendChild(tr);
  625. });
  626. if (todo.length > 500) {
  627. var more = document.createElement('tr');
  628. var td = document.createElement('td');
  629. td.colSpan = 4;
  630. td.className = 'orb-text-muted';
  631. td.textContent = t('js_more', { n: todo.length - 500 });
  632. more.appendChild(td);
  633. list.appendChild(more);
  634. }
  635. var size = todo.reduce(function (s, it) { return s + it.size; }, 0);
  636. sendBtn.disabled = todo.length === 0;
  637. sendLabel.textContent = todo.length ? t('js_send') + ' · ' + t('js_summary', { n: todo.length, size: bytes(size) }) : t('js_send');
  638. }
  639. ​
  640. async function hashFile(file) {
  641. return hex(await crypto.subtle.digest('SHA-256', await file.arrayBuffer()));
  642. }
  643. ​
  644. async function compare(group, withHash) {
  645. for (var i = 0; i < group.length; i += BATCH) {
  646. var slice = group.slice(i, i + BATCH);
  647. var data = await api('compare', { target: targetSel.value, files: slice.map(function (it) { return [it.path, it.size, withHash ? it.hash : '']; }) });
  648. if (!data.r) throw data;
  649. slice.forEach(function (it, k) { it.st = data.r[k]; });
  650. showProgress(t('js_comparing'), Math.min(i + BATCH, group.length), group.length);
  651. }
  652. }
  653. ​
  654. async function analyze(entries) {
  655. if (busy) return;
  656. if (!entries.length) { message('info', t('js_pick_first')); return; }
  657. setBusy(true);
  658. result.textContent = '';
  659. var first = entries[0].path.split('/')[0];
  660. var single = strip.checked && entries.every(function (e) { return e.path.indexOf(first + '/') === 0; });
  661. items = entries.map(function (e) {
  662. var p = single ? e.path.slice(first.length + 1) : e.path;
  663. return { file: e.file, path: p, size: e.file.size, st: '', hash: '' };
  664. }).filter(function (it) { return it.path !== ''; });
  665. items.forEach(function (it) { if (ALWAYS.test(it.path)) it.st = 'x'; });
  666. try {
  667. var check = items.filter(function (it) { return it.st === ''; });
  668. await compare(check, false);
  669. var toHash = items.filter(function (it) { return it.st === 'n' || it.st === 'c' || it.st === '?'; });
  670. if (toHash.length > MAX_FILES) { counts(); message('warning', t('js_too_many', { max: MAX_FILES })); return; }
  671. var done = 0;
  672. var queue = toHash.slice();
  673. var worker = async function () {
  674. while (queue.length) {
  675. var it = queue.shift();
  676. it.hash = it.size > MAX_SIZE ? '' : await hashFile(it.file);
  677. if (!it.hash) it.st = 'b';
  678. done++;
  679. if (done % 5 === 0 || done === toHash.length) showProgress(t('js_hashing'), done, toHash.length);
  680. }
  681. };
  682. await Promise.all([worker(), worker(), worker(), worker()]);
  683. var unsure = items.filter(function (it) { return it.st === '?'; });
  684. if (unsure.length) await compare(unsure, true);
  685. progress.hidden = true;
  686. var c = counts();
  687. renderList();
  688. if (!c.n && !c.c) message('success', t('js_nothing'));
  689. } catch (err) {
  690. progress.hidden = true;
  691. message('danger', errorText(err || {}));
  692. if (err && err._status === 401) setTimeout(function () { location.reload(); }, 2000);
  693. } finally {
  694. setBusy(false);
  695. }
  696. }
  697. ​
  698. async function uploadFile(id, index, it, onBytes) {
  699. var offset = 0;
  700. var attempts = 0;
  701. if (it.size === 0) {
  702. var r0 = await sendChunk(id, index, 0, new Blob([]));
  703. if (r0.have !== 0) throw r0;
  704. return;
  705. }
  706. while (offset < it.size) {
  707. var piece = it.file.slice(offset, Math.min(it.size, offset + CHUNK));
  708. var res;
  709. try {
  710. res = await sendChunk(id, index, offset, piece);
  711. } catch (e) {
  712. res = { _status: 0 };
  713. }
  714. if (res.have !== undefined && (res._status === 200 || res._status === 409)) {
  715. onBytes(res.have - offset);
  716. offset = res.have;
  717. attempts = 0;
  718. continue;
  719. }
  720. if (res._status === 401 || res._status === 403 || ++attempts > 4) throw res;
  721. await new Promise(function (r) { setTimeout(r, 600 * attempts); });
  722. }
  723. }
  724. ​
  725. function sendChunk(id, index, offset, blob) {
  726. return fetch(BASE + '?api=chunk', {
  727. method: 'POST',
  728. credentials: 'same-origin',
  729. headers: {
  730. 'Content-Type': 'application/octet-stream',
  731. 'X-SyncDrop-CSRF': CSRF,
  732. 'X-SyncDrop-Deploy': id,
  733. 'X-SyncDrop-File': String(index),
  734. 'X-SyncDrop-Offset': String(offset)
  735. },
  736. body: blob
  737. }).then(function (res) {
  738. return res.json().catch(function () { return {}; }).then(function (d) { d._status = res.status; return d; });
  739. });
  740. }
  741. ​
  742. async function send() {
  743. if (busy) return;
  744. var todo = pending();
  745. if (!todo.length) return;
  746. setBusy(true);
  747. sendBtn.disabled = true;
  748. sendBtn.classList.add('is-loading');
  749. result.textContent = '';
  750. var id = null;
  751. try {
  752. var begin = await api('begin', { target: targetSel.value, files: todo.map(function (it) { return [it.path, it.size, it.hash]; }) });
  753. if (!begin.id) throw begin;
  754. id = begin.id;
  755. var total = todo.reduce(function (s, it) { return s + it.size; }, 0) || 1;
  756. var sent = 0;
  757. var queue = todo.map(function (it, i) { return { it: it, i: i }; });
  758. var worker = async function () {
  759. while (queue.length) {
  760. var job = queue.shift();
  761. await uploadFile(id, job.i, job.it, function (n) { sent += n; showProgress(t('js_uploading'), sent, total); });
  762. }
  763. };
  764. showProgress(t('js_uploading'), 0, total);
  765. await Promise.all([worker(), worker(), worker()]);
  766. showProgress(t('js_verifying'), 1, 1);
  767. var commit = await api('commit', { id: id });
  768. if (!commit.ok) throw commit;
  769. id = null;
  770. todo.forEach(function (it) { it.st = 's'; });
  771. counts();
  772. renderList();
  773. progress.hidden = true;
  774. message('success', t('js_done', { n: commit['new'], c: commit.changed }));
  775. toast(t('js_done', { n: commit['new'], c: commit.changed }), 'success');
  776. } catch (err) {
  777. progress.hidden = true;
  778. message('danger', errorText(err || {}));
  779. if (id) api('abort', { id: id });
  780. if (err && err._status === 401) setTimeout(function () { location.reload(); }, 2000);
  781. sendBtn.disabled = false;
  782. } finally {
  783. sendBtn.classList.remove('is-loading');
  784. setBusy(false);
  785. }
  786. }
  787. ​
  788. $('[data-pick]').addEventListener('click', function () { if (!busy) picker.click(); });
  789. picker.addEventListener('change', function () {
  790. var entries = Array.prototype.map.call(picker.files, function (f) { return { file: f, path: (f.webkitRelativePath || f.name).replace(/\\/g, '/') }; });
  791. picker.value = '';
  792. analyze(entries);
  793. });
  794. drop.addEventListener('keydown', function (e) { if ((e.key === 'Enter' || e.key === ' ') && !busy) { e.preventDefault(); picker.click(); } });
  795. ['dragenter', 'dragover'].forEach(function (ev) {
  796. drop.addEventListener(ev, function (e) { e.preventDefault(); if (!busy) drop.classList.add('is-over'); });
  797. });
  798. ['dragleave', 'drop'].forEach(function (ev) {
  799. drop.addEventListener(ev, function () { drop.classList.remove('is-over'); });
  800. });
  801. drop.addEventListener('drop', async function (e) {
  802. e.preventDefault();
  803. if (busy || !e.dataTransfer) return;
  804. var roots = Array.prototype.map.call(e.dataTransfer.items || [], function (i) { return i.webkitGetAsEntry ? i.webkitGetAsEntry() : null; }).filter(Boolean);
  805. var out = [];
  806. for (var i = 0; i < roots.length; i++) await walk(roots[i], out);
  807. analyze(out);
  808. });
  809. targetSel.addEventListener('change', function () {
  810. items = [];
  811. counts();
  812. renderList();
  813. result.textContent = '';
  814. });
  815. sendBtn.addEventListener('click', send);
  816. }
  817. ​
  818. function init() {
  819. initPow();
  820. initConfirm();
  821. initCopy();
  822. initTimes();
  823. initSync();
  824. }
  825. ​
  826. if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  827. else init();
  828. })();
  829. ​