| 1 |
/* global jQuery, kingImageOptimizerAttachment */ |
| 2 |
(function ($) { |
| 3 |
'use strict'; |
| 4 |
// Prevent accidental tab close/navigation during upload-auto optimization |
| 5 |
let unloadGuardEnabled = false; |
| 6 |
function setUnloadGuard(enabled) { |
| 7 |
unloadGuardEnabled = !!enabled; |
| 8 |
} |
| 9 |
|
| 10 |
window.addEventListener('beforeunload', function (e) { |
| 11 |
if (!unloadGuardEnabled) return; |
| 12 |
e.preventDefault(); |
| 13 |
e.returnValue = 'A process is running. Are you sure you want to leave this page?'; |
| 14 |
return e.returnValue; |
| 15 |
}); |
| 16 |
|
| 17 |
// Toast UI (simple) |
| 18 |
let toastHideTimer = null; |
| 19 |
function ensureToast() { |
| 20 |
let $toast = $('#king-img-upload-toast'); |
| 21 |
if ($toast.length) return $toast; |
| 22 |
|
| 23 |
$toast = $( |
| 24 |
'<div id="king-img-upload-toast" class="king-img-toast" style="display:none;">' + |
| 25 |
'<div class="king-img-toast-head">' + |
| 26 |
'<span class="dashicons king-img-toast-icon dashicons-update" aria-hidden="true"></span>' + |
| 27 |
'<div class="king-img-toast-title">Optimizing uploads</div>' + |
| 28 |
'<button type="button" class="king-img-toast-close" aria-label="Close">×</button>' + |
| 29 |
'</div>' + |
| 30 |
'<div class="king-img-toast-body">' + |
| 31 |
'<div class="king-img-toast-line" id="king-img-toast-line">Preparing...</div>' + |
| 32 |
'<div class="king-img-toast-bar"><div class="king-img-toast-bar-fill" id="king-img-toast-bar-fill" style="width:0%"></div></div>' + |
| 33 |
'<div class="king-img-toast-meta" id="king-img-toast-meta">0 / 0</div>' + |
| 34 |
'</div>' + |
| 35 |
'</div>' |
| 36 |
); |
| 37 |
|
| 38 |
$(document.body).append($toast); |
| 39 |
$toast.on('click', '.king-img-toast-close', function () { |
| 40 |
$toast.fadeOut(200); |
| 41 |
}); |
| 42 |
|
| 43 |
return $toast; |
| 44 |
} |
| 45 |
|
| 46 |
function toastUpdate({ line, current, total, percent, status }) { |
| 47 |
const $toast = ensureToast(); |
| 48 |
$toast.show(); |
| 49 |
|
| 50 |
if (toastHideTimer) { |
| 51 |
window.clearTimeout(toastHideTimer); |
| 52 |
toastHideTimer = null; |
| 53 |
} |
| 54 |
|
| 55 |
if (typeof line !== 'undefined') { |
| 56 |
$('#king-img-toast-line').text(String(line)); |
| 57 |
} |
| 58 |
if (typeof current !== 'undefined' && typeof total !== 'undefined') { |
| 59 |
$('#king-img-toast-meta').text(String(current) + ' / ' + String(total)); |
| 60 |
} |
| 61 |
if (typeof percent !== 'undefined') { |
| 62 |
const p = Math.max(0, Math.min(100, Number(percent) || 0)); |
| 63 |
$('#king-img-toast-bar-fill').css('width', p + '%'); |
| 64 |
} |
| 65 |
|
| 66 |
const $icon = $toast.find('.king-img-toast-icon'); |
| 67 |
$toast.removeClass('is-success is-error is-warning'); |
| 68 |
if (status === 'success') { |
| 69 |
$toast.addClass('is-success'); |
| 70 |
$icon.removeClass().addClass('dashicons king-img-toast-icon dashicons-yes-alt'); |
| 71 |
toastHideTimer = window.setTimeout(function () { $toast.fadeOut(200); }, 5000); |
| 72 |
} else if (status === 'error') { |
| 73 |
$toast.addClass('is-error'); |
| 74 |
$icon.removeClass().addClass('dashicons king-img-toast-icon dashicons-dismiss'); |
| 75 |
toastHideTimer = window.setTimeout(function () { $toast.fadeOut(200); }, 5000); |
| 76 |
} else if (status === 'warning') { |
| 77 |
$toast.addClass('is-warning'); |
| 78 |
$icon.removeClass().addClass('dashicons king-img-toast-icon dashicons-warning'); |
| 79 |
toastHideTimer = window.setTimeout(function () { $toast.fadeOut(200); }, 5000); |
| 80 |
} else { |
| 81 |
$icon.removeClass().addClass('dashicons king-img-toast-icon dashicons-update'); |
| 82 |
} |
| 83 |
} |
| 84 |
|
| 85 |
function isMediaModalOpen() { |
| 86 |
try { |
| 87 |
return $('.media-modal:visible').length > 0; |
| 88 |
} catch (e) { |
| 89 |
return false; |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
async function refreshAttachmentOptimizerCard(attachmentId) { |
| 94 |
const id = Number(attachmentId || 0); |
| 95 |
if (!id) { |
| 96 |
return; |
| 97 |
} |
| 98 |
|
| 99 |
try { |
| 100 |
const res = await postAjax({ |
| 101 |
action: 'king_img_get_attachment_card_html', |
| 102 |
attachment_id: id, |
| 103 |
}); |
| 104 |
|
| 105 |
if (!res || !res.success || !res.data || !res.data.html) { |
| 106 |
return; |
| 107 |
} |
| 108 |
|
| 109 |
const $cards = $('.king-img-attach-card[data-king-img-attachment-id="' + String(id) + '"]'); |
| 110 |
if ($cards.length) { |
| 111 |
$cards.each(function () { |
| 112 |
$(this).replaceWith(res.data.html); |
| 113 |
}); |
| 114 |
} |
| 115 |
} catch (e) { |
| 116 |
// ignore |
| 117 |
} |
| 118 |
} |
| 119 |
|
| 120 |
function getAllBlocksDeep(blocks) { |
| 121 |
const out = []; |
| 122 |
const walk = function (arr) { |
| 123 |
(arr || []).forEach(function (b) { |
| 124 |
if (!b) return; |
| 125 |
out.push(b); |
| 126 |
if (b.innerBlocks && b.innerBlocks.length) { |
| 127 |
walk(b.innerBlocks); |
| 128 |
} |
| 129 |
}); |
| 130 |
}; |
| 131 |
walk(blocks || []); |
| 132 |
return out; |
| 133 |
} |
| 134 |
|
| 135 |
async function refreshGutenbergImageBlocksForAttachment(attachmentId) { |
| 136 |
const id = Number(attachmentId || 0); |
| 137 |
if (!id) return; |
| 138 |
|
| 139 |
if (!(window.wp && wp.data)) { |
| 140 |
return; |
| 141 |
} |
| 142 |
|
| 143 |
// Only relevant in the block editor. |
| 144 |
const blockEditor = wp.data.select('core/block-editor'); |
| 145 |
const blockEditorDispatch = wp.data.dispatch('core/block-editor'); |
| 146 |
if (!blockEditor || !blockEditorDispatch) { |
| 147 |
return; |
| 148 |
} |
| 149 |
|
| 150 |
let optimizedUrl = ''; |
| 151 |
|
| 152 |
// Prefer REST (same source Gutenberg uses for entities). |
| 153 |
try { |
| 154 |
if (wp.apiFetch) { |
| 155 |
const media = await wp.apiFetch({ path: '/wp/v2/media/' + String(id) }); |
| 156 |
if (media && media.source_url) { |
| 157 |
optimizedUrl = String(media.source_url); |
| 158 |
} |
| 159 |
} |
| 160 |
} catch (e) { |
| 161 |
// ignore |
| 162 |
} |
| 163 |
|
| 164 |
// Fallback to wp.media model. |
| 165 |
if (!optimizedUrl) { |
| 166 |
try { |
| 167 |
if (wp.media && wp.media.attachment) { |
| 168 |
const model = wp.media.attachment(id); |
| 169 |
// Ensure freshest data. |
| 170 |
if (model && model.fetch) { |
| 171 |
await new Promise((resolve) => { |
| 172 |
model.fetch({ |
| 173 |
success: function () { resolve(); }, |
| 174 |
error: function () { resolve(); }, |
| 175 |
}); |
| 176 |
}); |
| 177 |
} |
| 178 |
optimizedUrl = String((model && model.get && (model.get('url') || model.get('source_url'))) || ''); |
| 179 |
} |
| 180 |
} catch (e) { |
| 181 |
// ignore |
| 182 |
} |
| 183 |
} |
| 184 |
|
| 185 |
if (!optimizedUrl) { |
| 186 |
return; |
| 187 |
} |
| 188 |
|
| 189 |
// Update any core/image blocks using this attachment ID. |
| 190 |
try { |
| 191 |
const blocks = getAllBlocksDeep(blockEditor.getBlocks()); |
| 192 |
blocks.forEach(function (b) { |
| 193 |
if (!b || !b.clientId) return; |
| 194 |
if (b.name !== 'core/image') return; |
| 195 |
const bid = Number((b.attributes && b.attributes.id) || 0); |
| 196 |
if (bid !== id) return; |
| 197 |
|
| 198 |
blockEditorDispatch.updateBlockAttributes(b.clientId, { |
| 199 |
url: optimizedUrl, |
| 200 |
}); |
| 201 |
}); |
| 202 |
} catch (e) { |
| 203 |
// ignore |
| 204 |
} |
| 205 |
} |
| 206 |
|
| 207 |
function postAjax(data) { |
| 208 |
return $.ajax({ |
| 209 |
url: kingImageOptimizerAttachment.ajaxUrl, |
| 210 |
method: 'POST', |
| 211 |
dataType: 'json', |
| 212 |
data: Object.assign({ nonce: kingImageOptimizerAttachment.nonce }, data), |
| 213 |
}); |
| 214 |
} |
| 215 |
|
| 216 |
async function refreshSettingsFromServer() { |
| 217 |
try { |
| 218 |
const res = await postAjax({ |
| 219 |
action: 'king_img_get_settings', |
| 220 |
}); |
| 221 |
if (res && res.success && res.data && res.data.settings) { |
| 222 |
kingImageOptimizerAttachment.settings = res.data.settings; |
| 223 |
} |
| 224 |
} catch (e) { |
| 225 |
// ignore |
| 226 |
} |
| 227 |
} |
| 228 |
|
| 229 |
function setBusy($card, busy) { |
| 230 |
$card.toggleClass('is-busy', !!busy); |
| 231 |
$card.find('.king-img-attachment-action').prop('disabled', !!busy); |
| 232 |
} |
| 233 |
|
| 234 |
function setMsg($card, msg) { |
| 235 |
$card.find('.king-img-attach-msg').text(msg || ''); |
| 236 |
} |
| 237 |
|
| 238 |
function dataUrlFromBlob(blob) { |
| 239 |
return new Promise((resolve, reject) => { |
| 240 |
const reader = new FileReader(); |
| 241 |
reader.onload = () => resolve(String(reader.result || '')); |
| 242 |
reader.onerror = () => reject(new Error('Failed to read blob')); |
| 243 |
reader.readAsDataURL(blob); |
| 244 |
}); |
| 245 |
} |
| 246 |
|
| 247 |
function loadImage(url) { |
| 248 |
return new Promise((resolve, reject) => { |
| 249 |
const img = new Image(); |
| 250 |
img.onload = () => resolve(img); |
| 251 |
img.onerror = () => reject(new Error('Failed to load image')); |
| 252 |
img.crossOrigin = 'anonymous'; |
| 253 |
img.src = url; |
| 254 |
}); |
| 255 |
} |
| 256 |
|
| 257 |
async function convertOneSizeToWebp(imageInfo, quality, resizeEnabled, maxWidth) { |
| 258 |
const img = await loadImage(imageInfo.url); |
| 259 |
|
| 260 |
const srcW = img.naturalWidth || imageInfo.width || 0; |
| 261 |
const srcH = img.naturalHeight || imageInfo.height || 0; |
| 262 |
|
| 263 |
let outW = srcW; |
| 264 |
let outH = srcH; |
| 265 |
|
| 266 |
const mw = Number(maxWidth || 0); |
| 267 |
if (resizeEnabled && mw > 0 && srcW > mw) { |
| 268 |
const scale = mw / srcW; |
| 269 |
outW = mw; |
| 270 |
outH = Math.max(1, Math.round(srcH * scale)); |
| 271 |
} |
| 272 |
|
| 273 |
const canvas = document.createElement('canvas'); |
| 274 |
canvas.width = outW; |
| 275 |
canvas.height = outH; |
| 276 |
|
| 277 |
const ctx = canvas.getContext('2d', { alpha: true }); |
| 278 |
ctx.drawImage(img, 0, 0, outW, outH); |
| 279 |
|
| 280 |
const blob = await new Promise((resolve, reject) => { |
| 281 |
canvas.toBlob( |
| 282 |
(b) => (b ? resolve(b) : reject(new Error('toBlob returned null'))), |
| 283 |
'image/webp', |
| 284 |
quality |
| 285 |
); |
| 286 |
}); |
| 287 |
|
| 288 |
const dataUrl = await dataUrlFromBlob(blob); |
| 289 |
|
| 290 |
return { |
| 291 |
dataUrl, |
| 292 |
optimizedSize: blob.size, |
| 293 |
originalSize: Number(imageInfo.filesize || 0), |
| 294 |
}; |
| 295 |
} |
| 296 |
|
| 297 |
async function handleConvert($card) { |
| 298 |
const attachmentId = Number($card.data('king-img-attachment-id')); |
| 299 |
await refreshSettingsFromServer(); |
| 300 |
const settings = kingImageOptimizerAttachment.settings || {}; |
| 301 |
|
| 302 |
const qualityPct = Number(settings.quality || 82); |
| 303 |
const quality = Math.max(0.1, Math.min(1, qualityPct / 100)); |
| 304 |
|
| 305 |
const skipSmall = !!settings.skip_small; |
| 306 |
const minSize = Number(settings.min_size || 10240); |
| 307 |
|
| 308 |
const resizeEnabled = !!settings.resize_enabled; |
| 309 |
const maxWidth = Number(settings.max_width || 2048); |
| 310 |
|
| 311 |
setBusy($card, true); |
| 312 |
setMsg($card, kingImageOptimizerAttachment.strings.fetching || 'Fetching image data...'); |
| 313 |
|
| 314 |
const res = await postAjax({ |
| 315 |
action: 'king_img_get_image_data', |
| 316 |
attachment_id: attachmentId, |
| 317 |
sizes: 'all', |
| 318 |
}); |
| 319 |
|
| 320 |
if (!res || !res.success) { |
| 321 |
throw new Error((res && res.data && res.data.message) || 'Failed to fetch image data'); |
| 322 |
} |
| 323 |
|
| 324 |
const images = (res.data && res.data.images) || {}; |
| 325 |
const sizeNames = Object.keys(images); |
| 326 |
if (!sizeNames.length) { |
| 327 |
throw new Error('No image sizes found'); |
| 328 |
} |
| 329 |
|
| 330 |
let converted = 0; |
| 331 |
let skipped = 0; |
| 332 |
|
| 333 |
for (let i = 0; i < sizeNames.length; i++) { |
| 334 |
const size = sizeNames[i]; |
| 335 |
const info = images[size]; |
| 336 |
|
| 337 |
if (skipSmall && Number(info.filesize || 0) > 0 && Number(info.filesize || 0) < minSize) { |
| 338 |
skipped++; |
| 339 |
continue; |
| 340 |
} |
| 341 |
|
| 342 |
setMsg( |
| 343 |
$card, |
| 344 |
(kingImageOptimizerAttachment.strings.converting || 'Converting...') + |
| 345 |
' ' + |
| 346 |
size + |
| 347 |
' (' + |
| 348 |
String(i + 1) + |
| 349 |
'/' + |
| 350 |
String(sizeNames.length) + |
| 351 |
')' |
| 352 |
); |
| 353 |
|
| 354 |
const out = await convertOneSizeToWebp(info, quality, resizeEnabled, maxWidth); |
| 355 |
|
| 356 |
const saveRes = await postAjax({ |
| 357 |
action: 'king_img_save_optimized', |
| 358 |
attachment_id: attachmentId, |
| 359 |
size: size, |
| 360 |
format: 'webp', |
| 361 |
method: 'canvas', |
| 362 |
image_data: out.dataUrl, |
| 363 |
original_size: out.originalSize, |
| 364 |
optimized_size: out.optimizedSize, |
| 365 |
}); |
| 366 |
|
| 367 |
if (!saveRes || !saveRes.success) { |
| 368 |
if (saveRes && saveRes.data && saveRes.data.code === 'quota_exceeded') { |
| 369 |
setMsg( |
| 370 |
$card, |
| 371 |
kingImageOptimizerAttachment.strings.quotaExceeded || |
| 372 |
'Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.' |
| 373 |
); |
| 374 |
setBusy($card, false); |
| 375 |
return; |
| 376 |
} |
| 377 |
throw new Error((saveRes && saveRes.data && saveRes.data.message) || 'Failed to save optimized image'); |
| 378 |
} |
| 379 |
|
| 380 |
converted++; |
| 381 |
} |
| 382 |
|
| 383 |
if (converted === 0 && skipped > 0) { |
| 384 |
// Optional: persist skipped state so it doesn't show as pending elsewhere. |
| 385 |
await postAjax({ |
| 386 |
action: 'king_img_mark_skipped', |
| 387 |
attachment_id: attachmentId, |
| 388 |
reason: 'too_small', |
| 389 |
}).catch(() => {}); |
| 390 |
|
| 391 |
setMsg($card, kingImageOptimizerAttachment.strings.skipped || 'Skipped (too small)'); |
| 392 |
setBusy($card, false); |
| 393 |
return; |
| 394 |
} |
| 395 |
|
| 396 |
if (isMediaModalOpen()) { |
| 397 |
setMsg($card, kingImageOptimizerAttachment.strings.optimized || 'Optimized'); |
| 398 |
await refreshAttachmentOptimizerCard(attachmentId); |
| 399 |
await refreshGutenbergImageBlocksForAttachment(attachmentId); |
| 400 |
try { |
| 401 |
if (window.wp && wp.media && wp.media.attachment) { |
| 402 |
wp.media.attachment(attachmentId).fetch(); |
| 403 |
} |
| 404 |
} catch (e) { |
| 405 |
// ignore |
| 406 |
} |
| 407 |
setBusy($card, false); |
| 408 |
return; |
| 409 |
} |
| 410 |
|
| 411 |
setMsg($card, kingImageOptimizerAttachment.strings.doneReload || 'Done. Reloading...'); |
| 412 |
window.location.reload(); |
| 413 |
} |
| 414 |
|
| 415 |
async function handleRestore($card) { |
| 416 |
const attachmentId = Number($card.data('king-img-attachment-id')); |
| 417 |
|
| 418 |
const ok = window.confirm( |
| 419 |
kingImageOptimizerAttachment.strings.confirmRestore || 'Restore original and delete optimized files?' |
| 420 |
); |
| 421 |
if (!ok) return; |
| 422 |
|
| 423 |
setBusy($card, true); |
| 424 |
setMsg($card, kingImageOptimizerAttachment.strings.restoring || 'Restoring...'); |
| 425 |
|
| 426 |
const res = await postAjax({ |
| 427 |
action: 'king_img_full_restore', |
| 428 |
attachment_id: attachmentId, |
| 429 |
}); |
| 430 |
|
| 431 |
if (!res || !res.success) { |
| 432 |
throw new Error((res && res.data && res.data.message) || 'Restore failed'); |
| 433 |
} |
| 434 |
|
| 435 |
if (isMediaModalOpen()) { |
| 436 |
await refreshAttachmentOptimizerCard(attachmentId); |
| 437 |
await refreshGutenbergImageBlocksForAttachment(attachmentId); |
| 438 |
try { |
| 439 |
if (window.wp && wp.media && wp.media.attachment) { |
| 440 |
wp.media.attachment(attachmentId).fetch(); |
| 441 |
} |
| 442 |
} catch (e) { |
| 443 |
// ignore |
| 444 |
} |
| 445 |
setBusy($card, false); |
| 446 |
setMsg($card, kingImageOptimizerAttachment.strings.restored || 'Restored'); |
| 447 |
return; |
| 448 |
} |
| 449 |
|
| 450 |
setMsg($card, kingImageOptimizerAttachment.strings.doneReload || 'Done. Reloading...'); |
| 451 |
window.location.reload(); |
| 452 |
} |
| 453 |
|
| 454 |
$(document).on('click', '.king-img-attachment-action', async function () { |
| 455 |
const $btn = $(this); |
| 456 |
const $card = $btn.closest('.king-img-attach-card'); |
| 457 |
const action = String($btn.data('king-img-action') || ''); |
| 458 |
|
| 459 |
try { |
| 460 |
if (action === 'restore') { |
| 461 |
await handleRestore($card); |
| 462 |
} else { |
| 463 |
await handleConvert($card); |
| 464 |
} |
| 465 |
} catch (e) { |
| 466 |
setBusy($card, false); |
| 467 |
setMsg($card, (kingImageOptimizerAttachment.strings.errorPrefix || 'Error: ') + (e && e.message ? e.message : String(e))); |
| 468 |
} |
| 469 |
}); |
| 470 |
|
| 471 |
// --- Auto optimize new uploads (browser) --- |
| 472 |
const uploadAuto = { |
| 473 |
enabled: !!(kingImageOptimizerAttachment && kingImageOptimizerAttachment.settings && kingImageOptimizerAttachment.settings.auto_optimize_uploads), |
| 474 |
queue: [], |
| 475 |
queuedIds: new Set(), |
| 476 |
processedIds: new Set(), |
| 477 |
bound: false, |
| 478 |
running: false, |
| 479 |
total: 0, |
| 480 |
done: 0, |
| 481 |
errors: 0, |
| 482 |
}; |
| 483 |
|
| 484 |
async function convertAttachmentId(attachmentId, label) { |
| 485 |
const settings = kingImageOptimizerAttachment.settings || {}; |
| 486 |
|
| 487 |
const qualityPct = Number(settings.quality || 82); |
| 488 |
const quality = Math.max(0.1, Math.min(1, qualityPct / 100)); |
| 489 |
|
| 490 |
const skipSmall = !!settings.skip_small; |
| 491 |
const minSize = Number(settings.min_size || 10240); |
| 492 |
|
| 493 |
const resizeEnabled = !!settings.resize_enabled; |
| 494 |
const maxWidth = Number(settings.max_width || 2048); |
| 495 |
|
| 496 |
const autoReplaceUrls = settings.auto_replace_urls !== false; |
| 497 |
|
| 498 |
const res = await postAjax({ |
| 499 |
action: 'king_img_get_image_data', |
| 500 |
attachment_id: attachmentId, |
| 501 |
sizes: 'all', |
| 502 |
}); |
| 503 |
|
| 504 |
if (!res || !res.success) { |
| 505 |
throw new Error((res && res.data && res.data.message) || 'Failed to fetch image data'); |
| 506 |
} |
| 507 |
|
| 508 |
const images = (res.data && res.data.images) || {}; |
| 509 |
const sizeNames = Object.keys(images); |
| 510 |
if (!sizeNames.length) { |
| 511 |
throw new Error('No image sizes found'); |
| 512 |
} |
| 513 |
|
| 514 |
let converted = 0; |
| 515 |
let skipped = 0; |
| 516 |
|
| 517 |
for (let i = 0; i < sizeNames.length; i++) { |
| 518 |
const size = sizeNames[i]; |
| 519 |
const info = images[size]; |
| 520 |
|
| 521 |
if (skipSmall && Number(info.filesize || 0) > 0 && Number(info.filesize || 0) < minSize) { |
| 522 |
skipped++; |
| 523 |
continue; |
| 524 |
} |
| 525 |
|
| 526 |
toastUpdate({ |
| 527 |
line: `Converting ${label} • ${size} (${i + 1}/${sizeNames.length})`, |
| 528 |
}); |
| 529 |
|
| 530 |
const out = await convertOneSizeToWebp(info, quality, resizeEnabled, maxWidth); |
| 531 |
|
| 532 |
const saveRes = await postAjax({ |
| 533 |
action: 'king_img_save_optimized', |
| 534 |
attachment_id: attachmentId, |
| 535 |
size: size, |
| 536 |
format: 'webp', |
| 537 |
method: 'canvas', |
| 538 |
image_data: out.dataUrl, |
| 539 |
original_size: out.originalSize, |
| 540 |
optimized_size: out.optimizedSize, |
| 541 |
}); |
| 542 |
|
| 543 |
if (!saveRes || !saveRes.success) { |
| 544 |
if (saveRes && saveRes.data && saveRes.data.code === 'quota_exceeded') { |
| 545 |
setMsg( |
| 546 |
$card, |
| 547 |
kingImageOptimizerAttachment.strings.quotaExceeded || |
| 548 |
'Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.' |
| 549 |
); |
| 550 |
setBusy($card, false); |
| 551 |
return; |
| 552 |
} |
| 553 |
throw new Error((saveRes && saveRes.data && saveRes.data.message) || 'Failed to save optimized image'); |
| 554 |
} |
| 555 |
|
| 556 |
converted++; |
| 557 |
} |
| 558 |
|
| 559 |
if (converted === 0 && skipped > 0) { |
| 560 |
// Persist skipped state so it doesn't remain pending elsewhere. |
| 561 |
await postAjax({ |
| 562 |
action: 'king_img_mark_skipped', |
| 563 |
attachment_id: attachmentId, |
| 564 |
reason: 'too_small', |
| 565 |
}).catch(() => {}); |
| 566 |
return { status: 'skipped' }; |
| 567 |
} |
| 568 |
|
| 569 |
// Optionally apply URL replacements if enabled |
| 570 |
if (autoReplaceUrls) { |
| 571 |
await postAjax({ |
| 572 |
action: 'king_img_apply_webp_urls', |
| 573 |
attachment_id: attachmentId, |
| 574 |
}).catch(() => {}); |
| 575 |
} |
| 576 |
|
| 577 |
// Refresh the attachment model in media library if available |
| 578 |
try { |
| 579 |
if (window.wp && wp.media && wp.media.attachment) { |
| 580 |
wp.media.attachment(attachmentId).fetch(); |
| 581 |
} |
| 582 |
} catch (e) { |
| 583 |
// ignore |
| 584 |
} |
| 585 |
|
| 586 |
// Refresh the card in the modal/details UI if present |
| 587 |
await refreshAttachmentOptimizerCard(attachmentId); |
| 588 |
|
| 589 |
// Refresh Gutenberg block previews that reference this attachment |
| 590 |
await refreshGutenbergImageBlocksForAttachment(attachmentId); |
| 591 |
|
| 592 |
return { status: 'optimized' }; |
| 593 |
} |
| 594 |
|
| 595 |
function enqueueUploadedImage(attachmentId, label) { |
| 596 |
const id = Number(attachmentId || 0); |
| 597 |
if (!id) return; |
| 598 |
if (uploadAuto.processedIds.has(id) || uploadAuto.queuedIds.has(id)) return; |
| 599 |
|
| 600 |
uploadAuto.queue.push({ id, label: String(label || ('Attachment #' + id)) }); |
| 601 |
uploadAuto.queuedIds.add(id); |
| 602 |
uploadAuto.total++; |
| 603 |
|
| 604 |
if (!uploadAuto.running) { |
| 605 |
processUploadQueue(); |
| 606 |
} else { |
| 607 |
toastUpdate({ line: 'Added to queue: ' + String(label || id), current: uploadAuto.done, total: uploadAuto.total }); |
| 608 |
} |
| 609 |
} |
| 610 |
|
| 611 |
async function processUploadQueue() { |
| 612 |
if (uploadAuto.running) return; |
| 613 |
if (!uploadAuto.enabled) return; |
| 614 |
if (!uploadAuto.queue.length) return; |
| 615 |
|
| 616 |
uploadAuto.running = true; |
| 617 |
setUnloadGuard(true); |
| 618 |
|
| 619 |
// Ensure we use the latest saved settings (Default Quality, skip-small, etc) |
| 620 |
await refreshSettingsFromServer(); |
| 621 |
|
| 622 |
toastUpdate({ |
| 623 |
line: 'Starting…', |
| 624 |
current: uploadAuto.done, |
| 625 |
total: uploadAuto.total, |
| 626 |
percent: 0, |
| 627 |
status: 'running', |
| 628 |
}); |
| 629 |
|
| 630 |
while (uploadAuto.queue.length) { |
| 631 |
const item = uploadAuto.queue.shift(); |
| 632 |
const id = item.id; |
| 633 |
|
| 634 |
const currentIndex = uploadAuto.done + 1; |
| 635 |
const total = Math.max(uploadAuto.total, currentIndex); |
| 636 |
const percent = Math.round(((currentIndex - 1) / total) * 100); |
| 637 |
toastUpdate({ line: `Optimizing ${item.label}`, current: currentIndex - 1, total, percent }); |
| 638 |
|
| 639 |
try { |
| 640 |
await convertAttachmentId(id, item.label); |
| 641 |
uploadAuto.processedIds.add(id); |
| 642 |
} catch (e) { |
| 643 |
uploadAuto.errors++; |
| 644 |
} |
| 645 |
|
| 646 |
uploadAuto.done++; |
| 647 |
const p2 = Math.round((uploadAuto.done / uploadAuto.total) * 100); |
| 648 |
toastUpdate({ current: uploadAuto.done, total: uploadAuto.total, percent: p2 }); |
| 649 |
} |
| 650 |
|
| 651 |
// Done |
| 652 |
setUnloadGuard(false); |
| 653 |
uploadAuto.running = false; |
| 654 |
|
| 655 |
if (uploadAuto.errors > 0) { |
| 656 |
toastUpdate({ |
| 657 |
line: `Done with ${uploadAuto.errors} error(s).`, |
| 658 |
current: uploadAuto.done, |
| 659 |
total: uploadAuto.total, |
| 660 |
percent: 100, |
| 661 |
status: 'warning', |
| 662 |
}); |
| 663 |
} else { |
| 664 |
toastUpdate({ |
| 665 |
line: 'Done. All uploads optimized.', |
| 666 |
current: uploadAuto.done, |
| 667 |
total: uploadAuto.total, |
| 668 |
percent: 100, |
| 669 |
status: 'success', |
| 670 |
}); |
| 671 |
} |
| 672 |
} |
| 673 |
|
| 674 |
function initAutoOptimizeUploads() { |
| 675 |
if (!uploadAuto.enabled) return false; |
| 676 |
if (uploadAuto.bound) return true; |
| 677 |
|
| 678 |
// Hook into WP media attachments collection: new uploads get added here. |
| 679 |
try { |
| 680 |
if (window.wp && wp.media && wp.media.model && wp.media.model.Attachments && wp.media.model.Attachments.all) { |
| 681 |
const all = wp.media.model.Attachments.all; |
| 682 |
|
| 683 |
// Snapshot existing IDs so we don't auto-optimize the whole library on initial population. |
| 684 |
const knownIds = new Set(); |
| 685 |
try { |
| 686 |
const existingModels = all.models || []; |
| 687 |
for (let i = 0; i < existingModels.length; i++) { |
| 688 |
const m = existingModels[i]; |
| 689 |
if (m && m.get) { |
| 690 |
const id0 = Number(m.get('id') || 0); |
| 691 |
if (id0) knownIds.add(id0); |
| 692 |
} |
| 693 |
} |
| 694 |
} catch (e) { |
| 695 |
// ignore |
| 696 |
} |
| 697 |
|
| 698 |
all.on('add', function (model) { |
| 699 |
try { |
| 700 |
if (!model || !model.get) return; |
| 701 |
const type = model.get('type'); |
| 702 |
if (type !== 'image') return; |
| 703 |
|
| 704 |
const maybeEnqueue = function () { |
| 705 |
const id = Number(model.get('id') || 0); |
| 706 |
if (!id) return; |
| 707 |
if (knownIds.has(id)) return; |
| 708 |
knownIds.add(id); |
| 709 |
const filename = model.get('filename') || model.get('title') || ('Attachment #' + id); |
| 710 |
enqueueUploadedImage(id, filename); |
| 711 |
}; |
| 712 |
|
| 713 |
// Some upload models receive their attachment ID after upload completes. |
| 714 |
maybeEnqueue(); |
| 715 |
if (!Number(model.get('id') || 0)) { |
| 716 |
if (model.once) { |
| 717 |
model.once('change:id', maybeEnqueue); |
| 718 |
model.once('sync', maybeEnqueue); |
| 719 |
} |
| 720 |
} |
| 721 |
} catch (e) { |
| 722 |
// ignore |
| 723 |
} |
| 724 |
}); |
| 725 |
|
| 726 |
uploadAuto.bound = true; |
| 727 |
return true; |
| 728 |
} |
| 729 |
} catch (e) { |
| 730 |
// ignore |
| 731 |
} |
| 732 |
|
| 733 |
return false; |
| 734 |
} |
| 735 |
|
| 736 |
function initApiFetchMediaUploadHook() { |
| 737 |
if (!uploadAuto.enabled) return; |
| 738 |
if (!(window.wp && wp.apiFetch && typeof wp.apiFetch.use === 'function')) return; |
| 739 |
if (wp.apiFetch.__kingImgMediaUploadHooked) return; |
| 740 |
|
| 741 |
wp.apiFetch.__kingImgMediaUploadHooked = true; |
| 742 |
|
| 743 |
// Gutenberg/Image block direct uploads use wp.apiFetch POST /wp/v2/media |
| 744 |
wp.apiFetch.use(function (options, next) { |
| 745 |
const opts = options || {}; |
| 746 |
const method = String(opts.method || 'GET').toUpperCase(); |
| 747 |
const path = String(opts.path || opts.url || ''); |
| 748 |
const isMediaUpload = method === 'POST' && path.indexOf('/wp/v2/media') !== -1; |
| 749 |
|
| 750 |
let labelFromFile = ''; |
| 751 |
if (isMediaUpload) { |
| 752 |
try { |
| 753 |
const body = opts.body; |
| 754 |
if (body && typeof body.get === 'function') { |
| 755 |
const file = body.get('file'); |
| 756 |
if (file && file.name) { |
| 757 |
labelFromFile = String(file.name); |
| 758 |
} |
| 759 |
} |
| 760 |
} catch (e) { |
| 761 |
// ignore |
| 762 |
} |
| 763 |
} |
| 764 |
|
| 765 |
return next(options).then(function (result) { |
| 766 |
try { |
| 767 |
if (!isMediaUpload) { |
| 768 |
return result; |
| 769 |
} |
| 770 |
|
| 771 |
const id = Number(result && result.id ? result.id : 0); |
| 772 |
if (!id) { |
| 773 |
return result; |
| 774 |
} |
| 775 |
|
| 776 |
const mediaType = String((result && result.media_type) || ''); |
| 777 |
const mimeType = String((result && result.mime_type) || ''); |
| 778 |
const isImage = mediaType === 'image' || mimeType.indexOf('image/') === 0; |
| 779 |
if (!isImage) { |
| 780 |
return result; |
| 781 |
} |
| 782 |
|
| 783 |
const titleObj = result && result.title ? result.title : null; |
| 784 |
const title = titleObj && (titleObj.raw || titleObj.rendered) ? (titleObj.raw || titleObj.rendered) : ''; |
| 785 |
enqueueUploadedImage(id, labelFromFile || title || ('Attachment #' + id)); |
| 786 |
} catch (e) { |
| 787 |
// ignore |
| 788 |
} |
| 789 |
|
| 790 |
return result; |
| 791 |
}); |
| 792 |
}); |
| 793 |
} |
| 794 |
|
| 795 |
function scheduleAutoOptimizeUploadsInit() { |
| 796 |
if (!uploadAuto.enabled) return; |
| 797 |
if (uploadAuto.bound) return; |
| 798 |
|
| 799 |
// In the "Select or Upload Media" modal, wp.media collections are created lazily. |
| 800 |
// Retry a few times until wp.media.model.Attachments.all exists. |
| 801 |
let tries = 0; |
| 802 |
const timer = window.setInterval(function () { |
| 803 |
tries++; |
| 804 |
const ok = initAutoOptimizeUploads(); |
| 805 |
if (ok || tries >= 120) { |
| 806 |
window.clearInterval(timer); |
| 807 |
} |
| 808 |
}, 500); |
| 809 |
} |
| 810 |
|
| 811 |
$(document).ready(function () { |
| 812 |
initAutoOptimizeUploads(); |
| 813 |
scheduleAutoOptimizeUploadsInit(); |
| 814 |
initApiFetchMediaUploadHook(); |
| 815 |
}); |
| 816 |
})(jQuery); |
| 817 |
|