admin.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /*
  2. * Gallery bulk uploader.
  3. *
  4. * Per file, one request:
  5. * 1. draw a small JPEG thumbnail on a canvas (browser-side)
  6. * 2. POST the original + thumbnail to admin/api.php as multipart/form-data
  7. * 3. the webhost streams both to S3 and registers the image
  8. *
  9. * One file per request keeps each PHP process small, so the size of the whole
  10. * gallery upload never matters — only the largest single image. The browser
  11. * never sees an S3 URL or credential for writing.
  12. */
  13. (function () {
  14. 'use strict';
  15. var zone = document.getElementById('dropzone');
  16. if (!zone) return;
  17. var input = document.getElementById('file-input');
  18. var list = document.getElementById('upload-list');
  19. var countEl = document.getElementById('img-count');
  20. var api = zone.dataset.api;
  21. var slug = zone.dataset.slug;
  22. var csrf = zone.dataset.csrf;
  23. // Guest upload links pass a per-gallery key; the admin edit page sets none.
  24. var uploadKey = zone.dataset.key || '';
  25. var thumbSize = parseInt(zone.dataset.thumbSize, 10) || 600;
  26. var thumbQuality = parseFloat(zone.dataset.thumbQuality) || 0.8;
  27. var queue = [];
  28. var busy = false;
  29. zone.addEventListener('click', function () { input.click(); });
  30. input.addEventListener('change', function () { enqueue(input.files); input.value = ''; });
  31. ['dragenter', 'dragover'].forEach(function (ev) {
  32. zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.add('drag'); });
  33. });
  34. ['dragleave', 'drop'].forEach(function (ev) {
  35. zone.addEventListener(ev, function (e) { e.preventDefault(); zone.classList.remove('drag'); });
  36. });
  37. zone.addEventListener('drop', function (e) { enqueue(e.dataTransfer.files); });
  38. window.addEventListener('beforeunload', function (e) {
  39. if (busy || queue.length) { e.preventDefault(); e.returnValue = ''; }
  40. });
  41. function enqueue(files) {
  42. Array.prototype.forEach.call(files, function (file) {
  43. var row = document.createElement('div');
  44. row.className = 'upload-item';
  45. row.innerHTML = '<span class="name"></span><span class="bar"><i></i></span><span class="state">queued</span>';
  46. row.querySelector('.name').textContent = file.name;
  47. list.appendChild(row);
  48. queue.push({ file: file, row: row });
  49. });
  50. pump();
  51. }
  52. function pump() {
  53. if (busy || !queue.length) return;
  54. busy = true;
  55. var job = queue.shift();
  56. uploadOne(job.file, job.row)
  57. .then(function () { setState(job.row, 'done', 'done'); bumpCount(); })
  58. .catch(function (err) {
  59. setState(job.row, 'failed', 'error');
  60. job.row.title = String(err);
  61. var retry = document.createElement('a');
  62. retry.href = '#';
  63. retry.textContent = ' retry';
  64. retry.addEventListener('click', function (e) {
  65. e.preventDefault();
  66. retry.remove();
  67. setState(job.row, 'queued', '');
  68. queue.push(job);
  69. pump();
  70. });
  71. job.row.appendChild(retry);
  72. })
  73. .finally(function () { busy = false; pump(); });
  74. }
  75. function setState(row, text, cls) {
  76. var el = row.querySelector('.state');
  77. el.textContent = text;
  78. el.className = 'state ' + (cls || '');
  79. }
  80. function bumpCount() {
  81. if (countEl) countEl.textContent = String(parseInt(countEl.textContent, 10) + 1);
  82. }
  83. /* POST multipart to the webhost with upload progress (fetch has none → XHR). */
  84. function sendForm(form, onProgress) {
  85. return new Promise(function (resolve, reject) {
  86. var xhr = new XMLHttpRequest();
  87. xhr.open('POST', api);
  88. xhr.setRequestHeader('X-CSRF-Token', csrf);
  89. // Let the browser set Content-Type (with the multipart boundary).
  90. xhr.upload.addEventListener('progress', function (e) {
  91. if (e.lengthComputable && onProgress) onProgress(e.loaded / e.total);
  92. });
  93. xhr.addEventListener('load', function () {
  94. var data = {};
  95. try { data = JSON.parse(xhr.responseText); } catch (err) {}
  96. if (xhr.status >= 200 && xhr.status < 300 && data.ok) resolve(data);
  97. else reject(new Error((data && data.error) || ('Upload failed (' + xhr.status + ')')));
  98. });
  99. xhr.addEventListener('error', function () { reject(new Error('Network error during upload')); });
  100. xhr.send(form);
  101. });
  102. }
  103. /* Thumbnail as JPEG blob; null when the browser cannot decode the file
  104. (e.g. RAW) — the original still uploads untouched. */
  105. function makeThumb(file) {
  106. var decode = window.createImageBitmap
  107. ? createImageBitmap(file, { imageOrientation: 'from-image' })
  108. : new Promise(function (resolve, reject) {
  109. var img = new Image();
  110. img.onload = function () { resolve(img); };
  111. img.onerror = reject;
  112. img.src = URL.createObjectURL(file);
  113. });
  114. return decode.then(function (src) {
  115. var w = src.width, h = src.height;
  116. var scale = Math.min(1, thumbSize / Math.max(w, h));
  117. var canvas = document.createElement('canvas');
  118. canvas.width = Math.max(1, Math.round(w * scale));
  119. canvas.height = Math.max(1, Math.round(h * scale));
  120. canvas.getContext('2d').drawImage(src, 0, 0, canvas.width, canvas.height);
  121. if (src.close) src.close();
  122. return new Promise(function (resolve) {
  123. canvas.toBlob(function (blob) { resolve(blob); }, 'image/jpeg', thumbQuality);
  124. });
  125. }).catch(function () { return null; });
  126. }
  127. function uploadOne(file, row) {
  128. var bar = row.querySelector('.bar i');
  129. setState(row, 'thumbnail');
  130. return makeThumb(file).then(function (thumbBlob) {
  131. setState(row, 'uploading');
  132. var form = new FormData();
  133. form.append('slug', slug);
  134. if (uploadKey) form.append('key', uploadKey);
  135. form.append('original', file, file.name);
  136. if (thumbBlob) form.append('thumb', thumbBlob, 'thumb.jpg');
  137. return sendForm(form, function (f) {
  138. bar.style.width = Math.round(f * 100) + '%';
  139. });
  140. });
  141. }
  142. })();