admin.js 6.2 KB

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