PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Image_Optimizer / assets / bulk-optimizer.js

bulk-optimizer.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/extensions/Image_Optimizer/assets/bulk-optimizer.js

1,817 lines 63.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * King Addons - Bulk Image Optimizer
3 *
4 * Handles bulk optimization workflow, progress tracking,
5 * and UI interactions.
6 *
7 * @package King_Addons
8 */
9
10 (function($) {
11 'use strict';
12
13 function updateQuotaUI(quota) {
14 if (!quota || typeof quota !== 'object') {
15 return;
16 }
17
18 // Keep localized state in sync so other checks use fresh values.
19 if (typeof kingImageOptimizer !== 'undefined') {
20 kingImageOptimizer.quota = quota;
21 }
22
23 const $remaining = $('#ka-img-opt-quota-remaining');
24 const $limit = $('#ka-img-opt-quota-limit');
25
26 if ($remaining.length && typeof quota.remaining !== 'undefined') {
27 $remaining.text(String(quota.remaining));
28 }
29 if ($limit.length && typeof quota.limit !== 'undefined') {
30 $limit.text(String(quota.limit));
31 }
32 }
33
34 function showProLimitModal(context = {}) {
35 const $modal = $('#ka-img-opt-pro-modal');
36 if (!$modal.length) {
37 const msg = (kingImageOptimizer && kingImageOptimizer.strings && kingImageOptimizer.strings.quotaExceeded)
38 ? kingImageOptimizer.strings.quotaExceeded
39 : 'Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.';
40 alert(msg);
41 return;
42 }
43
44 const q = (context && context.quota) ? context.quota : (kingImageOptimizer ? kingImageOptimizer.quota : null);
45 if (q) {
46 $('#ka-img-opt-modal-remaining').text(String(typeof q.remaining !== 'undefined' ? q.remaining : 0));
47 $('#ka-img-opt-modal-limit').text(String(typeof q.limit !== 'undefined' ? q.limit : 200));
48 }
49
50 if (context && context.subtitle) {
51 $('#ka-img-opt-pro-modal-sub').text(String(context.subtitle));
52 }
53
54 // Upgrade URL can be overridden by server.
55 const upgradeUrl = (context && context.upgradeUrl)
56 ? context.upgradeUrl
57 : (kingImageOptimizer && kingImageOptimizer.upgradeUrl ? kingImageOptimizer.upgradeUrl : 'https://kingaddons.com/pricing/');
58 $('#ka-img-opt-modal-upgrade').attr('href', upgradeUrl);
59
60 $modal.attr('aria-hidden', 'false').show();
61 }
62
63 function hideProLimitModal() {
64 const $modal = $('#ka-img-opt-pro-modal');
65 if (!$modal.length) {
66 return;
67 }
68 $modal.attr('aria-hidden', 'true').hide();
69 }
70
71 function getQuotaRemaining() {
72 if (typeof kingImageOptimizer === 'undefined' || kingImageOptimizer.isPro) {
73 return Infinity;
74 }
75 const q = kingImageOptimizer.quota || {};
76 const remaining = parseInt(q.remaining, 10);
77 return Number.isFinite(remaining) ? remaining : 0;
78 }
79
80 // Prevent accidental tab close/navigation during long-running processes
81 let unloadGuardEnabled = false;
82 let unloadGuardMessage = '';
83
84 function setUnloadGuard(enabled, message) {
85 unloadGuardEnabled = !!enabled;
86 unloadGuardMessage = message || unloadGuardMessage || 'A process is running. Are you sure you want to leave this page?';
87 }
88
89 // Note: Most modern browsers ignore custom text and show a standard prompt.
90 window.addEventListener('beforeunload', function(e) {
91 if (!unloadGuardEnabled) {
92 return;
93 }
94
95 e.preventDefault();
96 // Chrome requires returnValue to be set.
97 e.returnValue = unloadGuardMessage;
98 return unloadGuardMessage;
99 });
100
101 // State management
102 let state = {
103 isProcessing: false,
104 isPaused: false,
105 shouldStop: false,
106 stopReason: null,
107 imageQueue: [],
108 imageIndex: {},
109 currentIndex: 0,
110 totalImages: 0,
111 successCount: 0,
112 skippedCount: 0,
113 errorCount: 0,
114 failedBase: null,
115 totalSavedBytes: 0,
116 startTime: 0,
117 results: []
118 };
119
120 // Live list UI state
121 const liveList = {
122 view: 'processed', // processed | remaining
123 filter: 'all', // all | success | skipped | error
124 page: 1,
125 perPage: 10
126 };
127
128 // Settings (canvas-only WebP conversion)
129 let settings = {
130 quality: 82,
131 skipSmall: false,
132 minSize: 10240,
133 autoReplaceUrls: true,
134 resizeEnabled: true,
135 maxWidth: 2048
136 };
137
138 function qualityToFillColor(quality) {
139 const q = Math.max(1, Math.min(100, parseInt(quality, 10) || 0));
140 // Map 1..100 => hue 10..120 (red -> green)
141 const t = (q - 1) / 99;
142 const hue = 10 + (110 * t);
143 return `hsl(${hue}, 85%, 45%)`;
144 }
145
146 function applySliderFill($slider, quality) {
147 if (!$slider || !$slider.length) {
148 return;
149 }
150
151 const q = Math.max(1, Math.min(100, parseInt(quality, 10) || 0));
152 const pct = ((q - 1) / 99) * 100;
153 const fillColor = qualityToFillColor(q);
154
155 const $wrap = $slider.closest('.ka-img-opt-slider-wrap');
156 if ($wrap.length) {
157 $wrap.css('--ka-slider-pct', pct.toFixed(2) + '%');
158 $wrap.css('--ka-slider-fill-color', fillColor);
159 }
160 }
161
162 function initSliderFills() {
163 $('.ka-img-opt-slider').each(function() {
164 const $slider = $(this);
165 applySliderFill($slider, $slider.val());
166 });
167 }
168
169 function setProgressTitleState(stateName) {
170 const $spinner = $('#ka-opt-progress-spinner');
171 const $check = $('#ka-opt-progress-check');
172
173 if (!$spinner.length || !$check.length) {
174 return;
175 }
176
177 if (stateName === 'running') {
178 $check.hide();
179 $spinner.removeClass('is-paused');
180 $spinner.addClass('is-active').show();
181 return;
182 }
183
184 if (stateName === 'paused') {
185 $check.hide();
186 $spinner.addClass('is-active is-paused').show();
187 return;
188 }
189
190 if (stateName === 'complete') {
191 $spinner.removeClass('is-active').hide();
192 $spinner.removeClass('is-paused');
193 $check.show();
194 return;
195 }
196
197 // idle / reset
198 $spinner.removeClass('is-active').hide();
199 $spinner.removeClass('is-paused');
200 $check.hide();
201 }
202
203 function parseIntFromText(text) {
204 const cleaned = String(text || '').replace(/[^0-9]/g, '');
205 const n = parseInt(cleaned, 10);
206 return Number.isFinite(n) ? n : 0;
207 }
208
209 function fetchGlobalStats() {
210 return new Promise((resolve) => {
211 $.ajax({
212 url: kingImageOptimizer.ajaxUrl,
213 type: 'POST',
214 data: {
215 action: 'king_img_get_stats',
216 nonce: kingImageOptimizer.nonce
217 },
218 success: function(response) {
219 if (response && response.success && response.data) {
220 resolve(response.data);
221 return;
222 }
223 resolve(null);
224 },
225 error: function() {
226 resolve(null);
227 }
228 });
229 });
230 }
231
232 function initLiveListControls() {
233 // Tabs
234 $(document).on('click', '.ka-img-opt-live-tab', function() {
235 const view = $(this).data('view');
236 if (!view) {
237 return;
238 }
239
240 liveList.view = view;
241 liveList.page = 1;
242
243 $('.ka-img-opt-live-tab').removeClass('active').attr('aria-selected', 'false');
244 $(this).addClass('active').attr('aria-selected', 'true');
245
246 if (liveList.view === 'remaining') {
247 $('.ka-img-opt-live-filters').css('opacity', '0.55');
248 $('.ka-img-opt-live-filter').prop('disabled', true);
249 } else {
250 $('.ka-img-opt-live-filters').css('opacity', '1');
251 $('.ka-img-opt-live-filter').prop('disabled', false);
252 }
253
254 renderLiveList();
255 });
256
257 // Filters
258 $(document).on('click', '.ka-img-opt-live-filter', function() {
259 const filter = $(this).data('filter');
260 if (!filter) {
261 return;
262 }
263
264 liveList.filter = filter;
265 liveList.page = 1;
266
267 $('.ka-img-opt-live-filter').removeClass('active');
268 $(this).addClass('active');
269 renderLiveList();
270 });
271
272 // Pagination
273 $('#ka-live-prev').on('click', function() {
274 liveList.page = Math.max(1, liveList.page - 1);
275 renderLiveList();
276 });
277
278 $('#ka-live-next').on('click', function() {
279 liveList.page = liveList.page + 1;
280 renderLiveList();
281 });
282 }
283
284 function buildImageIndex(images) {
285 state.imageIndex = {};
286 (images || []).forEach((img) => {
287 if (img && img.id) {
288 state.imageIndex[img.id] = img;
289 }
290 });
291 }
292
293 function escapeHtml(str) {
294 return String(str || '')
295 .replace(/&/g, '&')
296 .replace(/</g, '&lt;')
297 .replace(/>/g, '&gt;')
298 .replace(/"/g, '&quot;')
299 .replace(/'/g, '&#039;');
300 }
301
302 function getProcessedItems() {
303 const items = (state.results || []).slice().reverse();
304 return items
305 .filter((r) => {
306 if (liveList.filter === 'all') {
307 return true;
308 }
309 return r.status === liveList.filter;
310 })
311 .map((r) => {
312 const img = (state.imageIndex && r.id) ? state.imageIndex[r.id] : null;
313 return {
314 id: r.id,
315 title: (img && img.title) ? img.title : (r.filename || ''),
316 filename: r.filename || '',
317 thumb_url: (img && img.thumb_url) ? img.thumb_url : '',
318 status: r.status,
319 error: r.error,
320 savedBytes: r.savedBytes || 0,
321 originalBytes: r.originalBytes || 0,
322 optimizedBytes: r.optimizedBytes || 0,
323 savingsPercent: typeof r.savingsPercent !== 'undefined' ? r.savingsPercent : null,
324 mediaLink: r.id ? ('upload.php?item=' + r.id) : ''
325 };
326 });
327 }
328
329 function getRemainingItems() {
330 const remaining = (state.imageQueue || []).slice(state.currentIndex);
331 return remaining.map((img, idx) => ({
332 id: img.id,
333 title: img.title || img.filename,
334 filename: img.filename || '',
335 thumb_url: img.thumb_url || '',
336 status: (idx === 0 && state.isProcessing && !state.isPaused) ? 'current' : 'pending'
337 }));
338 }
339
340 function renderLiveList() {
341 const $list = $('#ka-live-list');
342 if (!$list.length) {
343 return;
344 }
345
346 const processedCount = (state.results || []).length;
347 const remainingCount = Math.max(0, (state.totalImages || 0) - (state.currentIndex || 0));
348 $('#ka-live-processed-count').text(processedCount);
349 $('#ka-live-remaining-count').text(remainingCount);
350
351 const items = (liveList.view === 'remaining') ? getRemainingItems() : getProcessedItems();
352 const total = items.length;
353 const perPage = liveList.perPage;
354 const totalPages = Math.max(1, Math.ceil(total / perPage));
355 liveList.page = Math.min(Math.max(1, liveList.page), totalPages);
356
357 const startIdx = (liveList.page - 1) * perPage;
358 const pageItems = items.slice(startIdx, startIdx + perPage);
359
360 $('#ka-live-page').text(liveList.page + ' / ' + totalPages);
361 $('#ka-live-prev').prop('disabled', liveList.page <= 1);
362 $('#ka-live-next').prop('disabled', liveList.page >= totalPages);
363
364 if (!pageItems.length) {
365 $list.html('<div class="ka-img-opt-live-item"><div class="ka-img-opt-live-meta"><div class="ka-img-opt-live-title">No items</div><div class="ka-img-opt-live-sub">Nothing to show yet.</div></div></div>');
366 return;
367 }
368
369 const html = pageItems.map((it) => {
370 const thumb = it.thumb_url
371 ? `<img src="${escapeHtml(it.thumb_url)}" alt="" loading="lazy" />`
372 : '';
373
374 const status = it.status || 'pending';
375 const pillClass = status;
376 const pillText = (status === 'success')
377 ? 'Optimized'
378 : (status === 'skipped')
379 ? 'Skipped'
380 : (status === 'error')
381 ? 'Failed'
382 : (status === 'current')
383 ? (state.isPaused ? 'Paused' : 'Current')
384 : 'Queued';
385
386 const sub = `#${it.id}${escapeHtml(it.filename || '')}`;
387
388 const hasSizeInfo = (status === 'success')
389 && Number.isFinite(it.originalBytes)
390 && it.originalBytes > 0
391 && Number.isFinite(it.optimizedBytes)
392 && it.optimizedBytes > 0;
393
394 const savingsPct = (hasSizeInfo)
395 ? (
396 (typeof it.savingsPercent === 'number' && Number.isFinite(it.savingsPercent))
397 ? it.savingsPercent
398 : Math.round(((it.originalBytes - it.optimizedBytes) / it.originalBytes) * 100)
399 )
400 : null;
401
402 const metrics = (hasSizeInfo)
403 ? `<div class="ka-img-opt-live-metrics" title="Original → New • Saved %">
404 <span class="ka-img-opt-live-size-old">${escapeHtml(KingImageOptimizer.formatBytes(it.originalBytes))}</span>
405 <span class="ka-img-opt-live-arrow">→</span>
406 <span class="ka-img-opt-live-size-new">${escapeHtml(KingImageOptimizer.formatBytes(it.optimizedBytes))}</span>
407 <span class="ka-img-opt-live-dot">•</span>
408 <span class="ka-img-opt-live-pct">${escapeHtml(String(savingsPct))}%</span>
409 </div>`
410 : '';
411
412 const mediaHref = it.mediaLink ? escapeHtml(it.mediaLink) : '';
413 const titleText = escapeHtml(it.filename || it.title || ('Attachment #' + it.id));
414 const title = mediaHref
415 ? `<a class="ka-img-opt-live-title-link" href="${mediaHref}" target="_blank" rel="noopener noreferrer">${titleText}</a>`
416 : titleText;
417
418 const errorHint = (status === 'error' && it.error)
419 ? ` title="${escapeHtml(it.error)}"`
420 : '';
421
422 return `
423 <div class="ka-img-opt-live-item"${errorHint}>
424 <div class="ka-img-opt-live-thumb">${thumb}</div>
425 <div class="ka-img-opt-live-meta">
426 <div class="ka-img-opt-live-title">${title}</div>
427 <div class="ka-img-opt-live-sub">${sub}</div>
428 </div>
429 <div class="ka-img-opt-live-right">
430 ${metrics}
431 <span class="ka-img-opt-pill ${pillClass}">${pillText}</span>
432 </div>
433 </div>
434 `;
435 }).join('');
436
437 $list.html(html);
438 }
439
440 /**
441 * Initialize bulk optimizer
442 */
443 function init() {
444 // Sync settings from server
445 if (typeof kingImageOptimizer !== 'undefined' && kingImageOptimizer.settings) {
446 settings.quality = kingImageOptimizer.settings.quality || settings.quality;
447 if (typeof kingImageOptimizer.settings.skip_small !== 'undefined') {
448 settings.skipSmall = !!parseInt(kingImageOptimizer.settings.skip_small, 10);
449 }
450 settings.minSize = parseInt(kingImageOptimizer.settings.min_size || settings.minSize, 10) || settings.minSize;
451 settings.autoReplaceUrls = kingImageOptimizer.settings.auto_replace_urls !== false;
452 settings.resizeEnabled = kingImageOptimizer.settings.resize_enabled || false;
453 settings.maxWidth = kingImageOptimizer.settings.max_width || 2048;
454 }
455
456 // Sync UI elements with settings
457 $('#auto-replace-urls').prop('checked', settings.autoReplaceUrls);
458 $('#skip-small').prop('checked', settings.skipSmall);
459 $('#resize-enabled').prop('checked', settings.resizeEnabled);
460 $('#max-width').val(settings.maxWidth);
461
462 // Settings tab mirrors (if present)
463 $('#settings-resize-enabled').prop('checked', settings.resizeEnabled);
464 $('#settings-max-width').val(settings.maxWidth);
465 $('.ka-img-opt-resize-options').toggle(!!settings.resizeEnabled);
466
467 bindEvents();
468
469 // Modal close handlers
470 $(document).on('click', '[data-ka-modal-close="1"]', function(e) {
471 e.preventDefault();
472 hideProLimitModal();
473 });
474
475 checkForSavedState();
476 initQualitySlider();
477 initSliderFills();
478 initLiveListControls();
479 renderLiveList();
480 }
481
482 /**
483 * Bind UI events
484 */
485 function bindEvents() {
486
487 // Quality presets
488 $('.ka-img-opt-preset-btn').on('click', function() {
489 $('.ka-img-opt-preset-btn').removeClass('active');
490 $(this).addClass('active');
491
492 const quality = parseInt($(this).data('quality'));
493 settings.quality = quality;
494 $('#quality-slider').val(quality);
495 $('#quality-output').text(quality + '%');
496 applySliderFill($('#quality-slider'), quality);
497 });
498
499 // Quality slider
500 $('#quality-slider').on('input', function() {
501 settings.quality = parseInt($(this).val());
502 $('#quality-output').text(settings.quality + '%');
503 updatePresetSelection(settings.quality);
504 applySliderFill($(this), settings.quality);
505 });
506
507 // Advanced settings toggle
508 $('#advanced-toggle').on('click', function() {
509 $('#advanced-content').slideToggle(300);
510 $(this).toggleClass('open');
511 });
512
513 // Skip small toggle
514 $('#skip-small').on('change', function() {
515 settings.skipSmall = $(this).is(':checked');
516 });
517
518 // Auto replace URLs toggle
519 $('#auto-replace-urls').on('change', function() {
520 settings.autoReplaceUrls = $(this).is(':checked');
521 });
522
523 // Resize toggle
524 $('#resize-enabled').on('change', function() {
525 settings.resizeEnabled = $(this).is(':checked');
526 $('.ka-img-opt-resize-options').toggle(settings.resizeEnabled);
527
528 // Mirror to settings tab
529 $('#settings-resize-enabled').prop('checked', settings.resizeEnabled);
530 });
531
532 // Settings tab resize toggle
533 $('#settings-resize-enabled').on('change', function() {
534 settings.resizeEnabled = $(this).is(':checked');
535 $('.ka-img-opt-resize-options').toggle(settings.resizeEnabled);
536
537 // Mirror to bulk advanced
538 $('#resize-enabled').prop('checked', settings.resizeEnabled);
539 });
540
541 // Max width input
542 $('#max-width').on('change', function() {
543 settings.maxWidth = parseInt($(this).val()) || 2048;
544
545 // Mirror to settings tab
546 $('#settings-max-width').val(settings.maxWidth);
547 });
548
549 // Settings tab max width
550 $('#settings-max-width').on('change', function() {
551 settings.maxWidth = parseInt($(this).val()) || 2048;
552
553 // Mirror to bulk advanced
554 $('#max-width').val(settings.maxWidth);
555 });
556
557
558 // Start optimization
559 $('#start-optimization').on('click', startOptimization);
560
561 // Pause button
562 $('#pause-btn').on('click', togglePause);
563
564 // Stop button
565 $('#stop-btn').on('click', stopOptimization);
566
567 // Resume button
568 $('#resume-btn').on('click', resumeOptimization);
569
570 // Discard session button
571 $('#discard-btn').on('click', discardSession);
572
573 // Resume close button
574 $('#resume-close').on('click', function() {
575 $('#resume-banner').slideUp(300);
576 });
577
578 // Optimize more button
579 $('#optimize-more').on('click', function() {
580 resetState();
581 $('#results-section').hide();
582 $('#optimization-options').show();
583 });
584
585 // Settings tab handlers
586 $('#settings-quality').on('input', function() {
587 const val = $(this).val();
588 $('#settings-quality-output').text(val + '%');
589 applySliderFill($(this), val);
590 });
591
592 $('#save-settings').on('click', saveSettings);
593 $('#restore-all').on('click', restoreAllImages);
594 $('#sync-media-library').on('click', syncMediaLibrary);
595 $('#sync-media-library-stop').on('click', stopSyncMediaLibrary);
596 }
597
598 // --- Media Library Sync ---
599 let syncQueue = [];
600 let syncTotal = 0;
601 let syncProcessed = 0;
602 let syncSynced = 0;
603 let syncSkipped = 0;
604 let syncErrors = 0;
605 let isSyncing = false;
606
607 function syncMediaLibrary() {
608 const $btn = $('#sync-media-library');
609 const $stop = $('#sync-media-library-stop');
610 const $progress = $('#sync-media-library-progress');
611
612 $btn.prop('disabled', true).html('<span class="ka-btn-spinner"></span> Restoring...');
613 $stop.hide();
614 $progress.hide();
615
616 $.ajax({
617 url: kingImageOptimizer.ajaxUrl,
618 type: 'POST',
619 data: {
620 action: 'king_img_get_sync_ids',
621 nonce: kingImageOptimizer.nonce
622 },
623 success: function(response) {
624 if (!response.success || !response.data || !response.data.ids) {
625 $btn.prop('disabled', false).html('<span class="dashicons dashicons-update"></span> Sync Media Library');
626 showNotification('Failed to load sync list.', 'error');
627 return;
628 }
629
630 syncQueue = response.data.ids;
631 syncTotal = response.data.total || syncQueue.length;
632 syncProcessed = 0;
633 syncSynced = 0;
634 syncSkipped = 0;
635 syncErrors = 0;
636 isSyncing = true;
637
638 setUnloadGuard(true, (typeof kingImageOptimizer !== 'undefined' && kingImageOptimizer.strings && kingImageOptimizer.strings.leaveWarning)
639 ? kingImageOptimizer.strings.leaveWarning
640 : 'A process is running. Are you sure you want to leave this page?');
641
642 if (syncTotal === 0) {
643 $btn.prop('disabled', false).html('<span class="dashicons dashicons-update"></span> Sync Media Library');
644 setUnloadGuard(false);
645 showNotification('No optimized images found to sync.', 'info');
646 return;
647 }
648
649 $btn.html('<span class="ka-btn-spinner"></span> Syncing...');
650 $stop.show();
651 $progress.show();
652 $('#sync-current-filename').text('Starting...');
653 updateSyncProgress();
654 processNextSyncBatch();
655 },
656 error: function() {
657 $btn.prop('disabled', false).html('<span class="dashicons dashicons-update"></span> Sync Media Library');
658 setUnloadGuard(false);
659 showNotification('Failed to load sync list.', 'error');
660 }
661 });
662 }
663
664 function stopSyncMediaLibrary() {
665 isSyncing = false;
666 setUnloadGuard(false);
667 $('#sync-media-library-stop').hide();
668 $('#sync-current-filename').text('Stopping...');
669 }
670
671 function updateSyncProgress() {
672 const percent = syncTotal > 0 ? Math.round((syncProcessed / syncTotal) * 100) : 0;
673 $('#sync-progress-fill').css('width', percent + '%');
674 $('#sync-progress-percent').text(percent + '%');
675 $('#sync-progress-count').text(syncProcessed + ' / ' + syncTotal);
676 }
677
678 function finishSyncMediaLibrary() {
679 const $btn = $('#sync-media-library');
680 const $stop = $('#sync-media-library-stop');
681
682 isSyncing = false;
683 setUnloadGuard(false);
684 $stop.hide();
685 $btn.prop('disabled', false).html('<span class="dashicons dashicons-update"></span> Sync Media Library');
686 $('#sync-current-filename').text('Done');
687
688 showNotification(
689 `Sync complete! Synced: ${syncSynced}, skipped: ${syncSkipped}, errors: ${syncErrors}.`,
690 syncErrors > 0 ? 'warning' : 'success'
691 );
692
693 // Refresh top stats
694 refreshStats();
695
696 // Refresh breakdown
697 refreshBreakdown();
698 }
699
700 function processNextSyncBatch() {
701 if (!isSyncing) {
702 finishSyncMediaLibrary();
703 return;
704 }
705
706 if (syncQueue.length === 0) {
707 finishSyncMediaLibrary();
708 return;
709 }
710
711 // Batch size (keep small to avoid timeouts)
712 const batchSize = 10;
713 const batch = syncQueue.splice(0, batchSize);
714
715 $('#sync-current-filename').text(`Syncing ${Math.min(syncProcessed + batch.length, syncTotal)} of ${syncTotal}...`);
716
717 $.ajax({
718 url: kingImageOptimizer.ajaxUrl,
719 type: 'POST',
720 data: {
721 action: 'king_img_sync_batch',
722 nonce: kingImageOptimizer.nonce,
723 ids: JSON.stringify(batch)
724 },
725 success: function(response) {
726 if (response.success && response.data) {
727 syncSynced += response.data.synced || 0;
728 syncSkipped += response.data.skipped || 0;
729 syncErrors += response.data.errors || 0;
730 } else {
731 syncErrors += batch.length;
732 }
733
734 syncProcessed += batch.length;
735 updateSyncProgress();
736
737 // Continue (avoid background tab throttling issues a bit)
738 const delay = (document.hidden || document.visibilityState === 'hidden') ? 500 : 0;
739 setTimeout(processNextSyncBatch, delay);
740 },
741 error: function() {
742 syncErrors += batch.length;
743 syncProcessed += batch.length;
744 updateSyncProgress();
745 const delay = (document.hidden || document.visibilityState === 'hidden') ? 500 : 0;
746 setTimeout(processNextSyncBatch, delay);
747 }
748 });
749 }
750
751 /**
752 * Initialize quality slider
753 */
754 function initQualitySlider() {
755 const quality = kingImageOptimizer.settings.quality || 82;
756 settings.quality = quality;
757 $('#quality-slider').val(quality);
758 $('#quality-output').text(quality + '%');
759 updatePresetSelection(quality);
760 applySliderFill($('#quality-slider'), quality);
761 }
762
763 /**
764 * Update preset button selection based on quality
765 */
766 function updatePresetSelection(quality) {
767 $('.ka-img-opt-preset-btn').removeClass('active');
768
769 // Find closest preset
770 let closest = null;
771 let minDiff = Infinity;
772
773 $('.ka-img-opt-preset-btn').each(function() {
774 const preset = parseInt($(this).data('quality'));
775 const diff = Math.abs(preset - quality);
776 if (diff < minDiff) {
777 minDiff = diff;
778 closest = $(this);
779 }
780 });
781
782 if (closest && minDiff <= 10) {
783 closest.addClass('active');
784 }
785 }
786
787 /**
788 * Check for saved optimization state
789 */
790 function checkForSavedState() {
791 $.ajax({
792 url: kingImageOptimizer.ajaxUrl,
793 type: 'POST',
794 data: {
795 action: 'king_img_get_state',
796 nonce: kingImageOptimizer.nonce
797 },
798 success: function(response) {
799 if (response.success && response.data.has_state) {
800 showResumeBanner(response.data.state);
801 }
802 }
803 });
804 }
805
806 /**
807 * Show resume banner
808 */
809 function showResumeBanner(savedState) {
810 // Check if optimization is already complete
811 if (savedState.currentIndex >= savedState.totalImages && savedState.totalImages > 0) {
812 // Already finished, clear the stale state silently
813 $.ajax({
814 url: kingImageOptimizer.ajaxUrl,
815 type: 'POST',
816 data: {
817 action: 'king_img_clear_state',
818 nonce: kingImageOptimizer.nonce
819 }
820 });
821 return;
822 }
823
824 $('#resume-count').text(savedState.currentIndex + ' of ' + savedState.totalImages);
825
826 // Show saved bytes if available
827 if (savedState.totalSavedBytes) {
828 $('#stat-saved').text(KingImageOptimizer.formatBytes(savedState.totalSavedBytes));
829 }
830
831 $('#resume-banner').slideDown(300);
832
833 // Store state for resume
834 state.savedState = savedState;
835 }
836
837 /**
838 * Resume optimization
839 */
840 async function resumeOptimization() {
841 if (!state.savedState) return;
842
843 // Restore state
844 state.imageQueue = state.savedState.imageQueue || [];
845 buildImageIndex(state.imageQueue);
846 state.currentIndex = state.savedState.currentIndex || 0;
847 state.totalImages = state.savedState.totalImages || 0;
848 state.successCount = state.savedState.successCount || 0;
849 state.errorCount = state.savedState.errorCount || 0;
850 state.totalSavedBytes = state.savedState.totalSavedBytes || 0;
851 settings = { ...settings, ...state.savedState.settings };
852
853 // Refresh baseline failed count so live updates don't double count
854 try {
855 const stats = await fetchGlobalStats();
856 const serverFailed = stats && typeof stats.failed_images !== 'undefined' ? parseInt(stats.failed_images, 10) || 0 : parseIntFromText($('#stat-failed').text());
857 state.failedBase = Math.max(0, serverFailed - (state.errorCount || 0));
858 } catch (e) {
859 state.failedBase = parseIntFromText($('#stat-failed').text());
860 }
861
862 // Check if already complete
863 if (state.currentIndex >= state.totalImages) {
864 // Already finished, clear state and show completion
865 discardSessionSilent();
866 $('#resume-banner').slideUp(300);
867 return;
868 }
869
870 // Hide resume banner and options
871 $('#resume-banner').slideUp(300);
872 $('#optimization-options').slideUp(300);
873
874 // Show progress
875 $('#progress-section').slideDown(300);
876
877 // Title spinner while running
878 setProgressTitleState('running');
879
880 // Update progress display with saved values
881 updateProgress();
882 renderLiveList();
883
884 // Start processing
885 state.isProcessing = true;
886 state.isPaused = false;
887 state.shouldStop = false;
888 state.startTime = Date.now();
889
890 processNextImage();
891 }
892
893 /**
894 * Discard session
895 */
896 function discardSession() {
897 if (!confirm(kingImageOptimizer.strings.confirmBulkRestore || 'Are you sure you want to discard the saved session?')) {
898 return;
899 }
900 discardSessionSilent();
901 }
902
903 /**
904 * Discard session silently (no confirmation)
905 */
906 function discardSessionSilent() {
907 $.ajax({
908 url: kingImageOptimizer.ajaxUrl,
909 type: 'POST',
910 data: {
911 action: 'king_img_clear_state',
912 nonce: kingImageOptimizer.nonce
913 },
914 success: function() {
915 $('#resume-banner').slideUp(300);
916 state.savedState = null;
917 }
918 });
919 }
920
921 /**
922 * Start optimization
923 */
924 async function startOptimization() {
925 const remaining = getQuotaRemaining();
926 if (remaining <= 0) {
927 showProLimitModal({
928 quota: (kingImageOptimizer ? kingImageOptimizer.quota : null),
929 subtitle: (kingImageOptimizer && kingImageOptimizer.strings && kingImageOptimizer.strings.quotaExceeded)
930 ? kingImageOptimizer.strings.quotaExceeded
931 : 'Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.',
932 });
933 return;
934 }
935
936 setUnloadGuard(true, (typeof kingImageOptimizer !== 'undefined' && kingImageOptimizer.strings && kingImageOptimizer.strings.leaveWarning)
937 ? kingImageOptimizer.strings.leaveWarning
938 : 'Optimization is running. Are you sure you want to leave this page?');
939
940 resetState();
941
942 // Baseline failed count for live updates
943 try {
944 const stats = await fetchGlobalStats();
945 const serverFailed = stats && typeof stats.failed_images !== 'undefined' ? parseInt(stats.failed_images, 10) || 0 : parseIntFromText($('#stat-failed').text());
946 state.failedBase = Math.max(0, serverFailed - state.errorCount);
947 } catch (e) {
948 state.failedBase = parseIntFromText($('#stat-failed').text());
949 }
950
951 // Show progress section
952 $('#optimization-options').slideUp(300);
953 $('#progress-section').slideDown(300);
954
955 // Title spinner while running
956 setProgressTitleState('running');
957
958 // Fetch images to optimize
959 try {
960 const images = await fetchImagesToOptimize();
961
962 if (images.length === 0) {
963 setUnloadGuard(false);
964 showError('No images found to optimize.');
965 return;
966 }
967
968 state.imageQueue = images;
969 buildImageIndex(images);
970 state.totalImages = images.length;
971 state.isProcessing = true;
972 state.startTime = Date.now();
973
974 updateProgress();
975 renderLiveList();
976 processNextImage();
977
978 } catch (error) {
979 setUnloadGuard(false);
980 showError('Failed to fetch images: ' + error.message);
981 }
982 }
983
984 /**
985 * Fetch images to optimize
986 */
987 function fetchImagesToOptimize() {
988 return new Promise((resolve, reject) => {
989 $.ajax({
990 url: kingImageOptimizer.ajaxUrl,
991 type: 'POST',
992 data: {
993 action: 'king_img_get_bulk_images',
994 nonce: kingImageOptimizer.nonce,
995 filter: 'pending',
996 per_page: 1000
997 },
998 success: function(response) {
999 if (response.success) {
1000 resolve(response.data.images);
1001 } else {
1002 reject(new Error(response.data.message || 'Failed to fetch images'));
1003 }
1004 },
1005 error: function(xhr, status, error) {
1006 reject(new Error(error));
1007 }
1008 });
1009 });
1010 }
1011
1012 /**
1013 * Process next image in queue
1014 */
1015 async function processNextImage() {
1016 // Check stop condition
1017 if (state.shouldStop || state.currentIndex >= state.totalImages) {
1018 finishOptimization();
1019 return;
1020 }
1021
1022 // Check pause
1023 if (state.isPaused) {
1024 return;
1025 }
1026
1027 const image = state.imageQueue[state.currentIndex];
1028
1029 // Update UI
1030 $('#current-filename').text(image.filename);
1031
1032 try {
1033 // Get image data
1034 const imageData = await getImageData(image.id);
1035
1036 let imageSavedBytes = 0;
1037 let fullOriginalBytes = 0;
1038 let fullOptimizedBytes = 0;
1039
1040 let didSaveAny = false;
1041 let hadAnyEligibleSize = false;
1042
1043 // Process each size
1044 for (const [sizeName, sizeData] of Object.entries(imageData.images)) {
1045 // Skip small files if enabled
1046 if (settings.skipSmall && sizeData.filesize < settings.minSize) {
1047 continue;
1048 }
1049
1050 hadAnyEligibleSize = true;
1051
1052 // Optimize
1053 const result = await window.kingOptimizer.optimize({
1054 url: sizeData.url,
1055 filesize: sizeData.filesize,
1056 mime_type: sizeData.mime_type,
1057 width: sizeData.width,
1058 height: sizeData.height
1059 }, {
1060 quality: settings.quality,
1061 resize: settings.resizeEnabled,
1062 maxWidth: settings.maxWidth
1063 });
1064
1065 if (result.success) {
1066 // Save optimized image
1067 const saved = await saveOptimizedImage(image.id, sizeName, result);
1068
1069 const savedBytes = (saved && typeof saved.saved_bytes !== 'undefined')
1070 ? (parseInt(saved.saved_bytes, 10) || 0)
1071 : (result.savedBytes || 0);
1072
1073 state.totalSavedBytes += savedBytes;
1074 imageSavedBytes += savedBytes;
1075
1076 if (sizeName === 'full') {
1077 fullOriginalBytes = (saved && typeof saved.original_size !== 'undefined')
1078 ? (parseInt(saved.original_size, 10) || 0)
1079 : (result.originalSize || sizeData.filesize || 0);
1080 fullOptimizedBytes = (saved && typeof saved.optimized_size !== 'undefined')
1081 ? (parseInt(saved.optimized_size, 10) || 0)
1082 : (result.optimizedSize || 0);
1083 }
1084
1085 didSaveAny = true;
1086 }
1087 }
1088
1089 if (didSaveAny) {
1090 // Apply WebP URLs if enabled
1091 if (settings.autoReplaceUrls) {
1092 await applyWebpUrls(image.id);
1093 }
1094
1095 state.successCount++;
1096 state.results.push({
1097 id: image.id,
1098 filename: image.filename,
1099 status: 'success',
1100 savedBytes: imageSavedBytes,
1101 originalBytes: fullOriginalBytes,
1102 optimizedBytes: fullOptimizedBytes,
1103 savingsPercent: fullOriginalBytes > 0 ? Math.round(((fullOriginalBytes - fullOptimizedBytes) / fullOriginalBytes) * 100) : 0
1104 });
1105 } else {
1106 // Nothing saved (usually everything skipped by "Skip Small")
1107 state.skippedCount++;
1108
1109 // Persist skipped images so they don't keep reappearing as Pending
1110 if (!hadAnyEligibleSize && settings.skipSmall) {
1111 await markSkipped(image.id, 'skip_small');
1112 }
1113
1114 state.results.push({
1115 id: image.id,
1116 filename: image.filename,
1117 status: 'skipped'
1118 });
1119 }
1120
1121 // If the free quota just ran out, stop before moving to next image.
1122 const remainingAfter = getQuotaRemaining();
1123 if (!kingImageOptimizer.isPro && remainingAfter <= 0 && (state.currentIndex + 1) < state.totalImages) {
1124 state.stopReason = 'quota';
1125 state.shouldStop = true;
1126 }
1127
1128 } catch (error) {
1129 // console.error('Error optimizing image:', error);
1130
1131 if (error && error.code === 'quota_exceeded') {
1132 state.stopReason = 'quota';
1133 state.shouldStop = true;
1134 state.isProcessing = false;
1135 state.isPaused = false;
1136 setUnloadGuard(false);
1137
1138 // Keep server resume state so user can upgrade and continue later.
1139 setTimeout(() => {
1140 showProLimitModal({
1141 quota: (kingImageOptimizer ? kingImageOptimizer.quota : null),
1142 subtitle: (kingImageOptimizer && kingImageOptimizer.strings && kingImageOptimizer.strings.quotaExceeded)
1143 ? kingImageOptimizer.strings.quotaExceeded
1144 : 'Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.',
1145 upgradeUrl: error.upgradeUrl || (kingImageOptimizer ? kingImageOptimizer.upgradeUrl : null),
1146 });
1147 }, 250);
1148
1149 finishOptimization();
1150 return;
1151 }
1152
1153 state.errorCount++;
1154
1155 // Persist failed images so they don't keep reappearing as Pending
1156 try {
1157 await markFailed(image.id, error && error.message ? error.message : 'Optimization error');
1158 } catch (e) {
1159 // best-effort only
1160 }
1161
1162 state.results.push({
1163 id: image.id,
1164 filename: image.filename,
1165 status: 'error',
1166 error: error.message
1167 });
1168 }
1169
1170 // Move to next
1171 state.currentIndex++;
1172 updateProgress();
1173 renderLiveList();
1174 saveOptimizationState();
1175
1176 if (state.stopReason === 'quota') {
1177 finishOptimization();
1178 setTimeout(() => {
1179 showProLimitModal({
1180 quota: (kingImageOptimizer ? kingImageOptimizer.quota : null),
1181 subtitle: 'Free plan limit reached (200 optimizations/month). Upgrade to Unlimited to continue.',
1182 });
1183 }, 350);
1184 return;
1185 }
1186
1187 if (state.shouldStop) {
1188 finishOptimization();
1189 return;
1190 }
1191
1192 // Continue with next image
1193 // NOTE: requestAnimationFrame is heavily throttled/paused in background tabs.
1194 // Use a timer so bulk optimization can continue (though browsers may still throttle).
1195 const delay = (document.hidden || document.visibilityState === 'hidden') ? 1000 : 0;
1196 setTimeout(() => processNextImage(), delay);
1197 }
1198
1199 /**
1200 * Get image data from server
1201 */
1202 function getImageData(attachmentId) {
1203 return new Promise((resolve, reject) => {
1204 $.ajax({
1205 url: kingImageOptimizer.ajaxUrl,
1206 type: 'POST',
1207 data: {
1208 action: 'king_img_get_image_data',
1209 nonce: kingImageOptimizer.nonce,
1210 attachment_id: attachmentId,
1211 sizes: 'all'
1212 },
1213 success: function(response) {
1214 if (response.success) {
1215 resolve(response.data);
1216 } else {
1217 reject(new Error(response.data.message || 'Failed to get image data'));
1218 }
1219 },
1220 error: function(xhr, status, error) {
1221 reject(new Error(error));
1222 }
1223 });
1224 });
1225 }
1226
1227 /**
1228 * Save optimized image to server
1229 */
1230 function saveOptimizedImage(attachmentId, size, result) {
1231 return new Promise((resolve, reject) => {
1232 $.ajax({
1233 url: kingImageOptimizer.ajaxUrl,
1234 type: 'POST',
1235 data: {
1236 action: 'king_img_save_optimized',
1237 nonce: kingImageOptimizer.nonce,
1238 attachment_id: attachmentId,
1239 size: size,
1240 format: result.format,
1241 image_data: result.data,
1242 original_size: result.originalSize,
1243 optimized_size: result.optimizedSize,
1244 method: result.method
1245 },
1246 success: function(response) {
1247 if (response.success) {
1248 if (response.data && response.data.quota) {
1249 updateQuotaUI(response.data.quota);
1250 }
1251 resolve(response.data);
1252 } else {
1253 if (response.data && response.data.quota) {
1254 updateQuotaUI(response.data.quota);
1255 }
1256
1257 const err = new Error((response.data && response.data.message) ? response.data.message : 'Failed to save image');
1258 if (response.data && response.data.code) {
1259 err.code = response.data.code;
1260 }
1261 reject(err);
1262 }
1263 },
1264 error: function(xhr, status, error) {
1265 const json = xhr && xhr.responseJSON ? xhr.responseJSON : null;
1266 if (json && json.success === false && json.data) {
1267 if (json.data.quota) {
1268 updateQuotaUI(json.data.quota);
1269 }
1270
1271 const err = new Error(json.data.message || error || 'Request failed');
1272 if (json.data.code) {
1273 err.code = json.data.code;
1274 }
1275 if (json.data.upgrade_url) {
1276 err.upgradeUrl = json.data.upgrade_url;
1277 }
1278 reject(err);
1279 return;
1280 }
1281
1282 reject(new Error(error));
1283 }
1284 });
1285 });
1286 }
1287
1288 /**
1289 * Apply WebP URLs to database
1290 */
1291 function applyWebpUrls(attachmentId) {
1292 return new Promise((resolve, reject) => {
1293 $.ajax({
1294 url: kingImageOptimizer.ajaxUrl,
1295 type: 'POST',
1296 data: {
1297 action: 'king_img_apply_webp_urls',
1298 nonce: kingImageOptimizer.nonce,
1299 attachment_id: attachmentId
1300 },
1301 success: function(response) {
1302 resolve(response.success);
1303 },
1304 error: function() {
1305 resolve(false); // Don't fail optimization for URL replacement errors
1306 }
1307 });
1308 });
1309 }
1310
1311 /**
1312 * Mark an attachment as skipped in DB so it doesn't reappear as pending.
1313 */
1314 function markSkipped(attachmentId, reason) {
1315 return new Promise((resolve) => {
1316 $.ajax({
1317 url: kingImageOptimizer.ajaxUrl,
1318 type: 'POST',
1319 data: {
1320 action: 'king_img_mark_skipped',
1321 nonce: kingImageOptimizer.nonce,
1322 attachment_id: attachmentId,
1323 reason: reason || 'skipped'
1324 },
1325 success: function() {
1326 resolve(true);
1327 },
1328 error: function() {
1329 resolve(false);
1330 }
1331 });
1332 });
1333 }
1334
1335 /**
1336 * Mark an attachment as failed in DB so it doesn't reappear as pending.
1337 */
1338 function markFailed(attachmentId, reason) {
1339 return new Promise((resolve) => {
1340 $.ajax({
1341 url: kingImageOptimizer.ajaxUrl,
1342 type: 'POST',
1343 data: {
1344 action: 'king_img_mark_failed',
1345 nonce: kingImageOptimizer.nonce,
1346 attachment_id: attachmentId,
1347 reason: reason || 'Optimization failed'
1348 },
1349 success: function() {
1350 resolve(true);
1351 },
1352 error: function() {
1353 resolve(false);
1354 }
1355 });
1356 });
1357 }
1358
1359 /**
1360 * Update progress UI
1361 */
1362 function updateProgress() {
1363 const percent = state.totalImages > 0
1364 ? Math.round((state.currentIndex / state.totalImages) * 100)
1365 : 0;
1366
1367 $('#progress-fill').css('width', percent + '%');
1368 $('#progress-percent').text(percent + '%');
1369 $('#progress-count').text(state.currentIndex + ' / ' + state.totalImages);
1370
1371 $('#live-success').text(state.successCount);
1372 $('#live-skipped').text(state.skippedCount);
1373 $('#live-errors').text(state.errorCount);
1374 $('#live-saved').text(KingImageOptimizer.formatBytes(state.totalSavedBytes));
1375
1376 // Update stats
1377 $('#stat-optimized').text(state.successCount);
1378 $('#stat-saved').text(KingImageOptimizer.formatBytes(state.totalSavedBytes));
1379
1380 // Update failed stat live during processing
1381 if ($('#stat-failed').length) {
1382 const base = (typeof state.failedBase === 'number' && Number.isFinite(state.failedBase))
1383 ? state.failedBase
1384 : parseIntFromText($('#stat-failed').text());
1385 const displayFailed = Math.max(0, base + (state.errorCount || 0));
1386 $('#stat-failed').text(displayFailed.toLocaleString());
1387 }
1388 }
1389
1390 /**
1391 * Toggle pause
1392 */
1393 function togglePause() {
1394 state.isPaused = !state.isPaused;
1395
1396 if (state.isPaused) {
1397 $('#pause-btn').html('<span class="dashicons dashicons-controls-play"></span> ' +
1398 (kingImageOptimizer.strings.resume || 'Resume'));
1399 saveOptimizationState();
1400
1401 // Stop the title spinner animation while paused
1402 setProgressTitleState('paused');
1403
1404 refreshStats();
1405 refreshBreakdown();
1406 renderLiveList();
1407 } else {
1408 $('#pause-btn').html('<span class="dashicons dashicons-controls-pause"></span> ' +
1409 (kingImageOptimizer.strings.pause || 'Pause'));
1410
1411 // Resume the title spinner animation
1412 setProgressTitleState('running');
1413 renderLiveList();
1414 processNextImage();
1415 }
1416 }
1417
1418 /**
1419 * Stop optimization
1420 */
1421 function stopOptimization() {
1422 state.stopReason = 'user';
1423 state.shouldStop = true;
1424
1425 // If paused, resume the loop just to allow it to exit cleanly.
1426 if (state.isPaused) {
1427 state.isPaused = false;
1428 $('#pause-btn').html('<span class="dashicons dashicons-controls-pause"></span> ' +
1429 (kingImageOptimizer.strings.pause || 'Pause'));
1430
1431 setProgressTitleState('running');
1432 processNextImage();
1433 return;
1434 }
1435
1436 // For in-flight work, Stop completes after current image finishes.
1437 renderLiveList();
1438 }
1439
1440 /**
1441 * Save optimization state for resume
1442 */
1443 function saveOptimizationState() {
1444 $.ajax({
1445 url: kingImageOptimizer.ajaxUrl,
1446 type: 'POST',
1447 data: {
1448 action: 'king_img_save_state',
1449 nonce: kingImageOptimizer.nonce,
1450 currentIndex: state.currentIndex,
1451 totalImages: state.totalImages,
1452 successCount: state.successCount,
1453 errorCount: state.errorCount,
1454 totalSavedBytes: state.totalSavedBytes,
1455 imageQueue: JSON.stringify(state.imageQueue),
1456 settings: JSON.stringify(settings)
1457 }
1458 });
1459 }
1460 /**
1461 * Finish optimization
1462 */
1463 function finishOptimization() {
1464 state.isProcessing = false;
1465 setUnloadGuard(false);
1466 if (state.stopReason !== 'quota') {
1467 state.savedState = null; // Clear local saved state
1468 }
1469
1470 // Title check when finished
1471 setProgressTitleState('complete');
1472
1473 // Clear saved state on server only when fully complete.
1474 if (state.stopReason !== 'quota') {
1475 $.ajax({
1476 url: kingImageOptimizer.ajaxUrl,
1477 type: 'POST',
1478 data: {
1479 action: 'king_img_clear_state',
1480 nonce: kingImageOptimizer.nonce
1481 }
1482 });
1483 }
1484
1485 // Hide resume banner if visible (keep it for quota stop so user can resume after upgrade)
1486 if (state.stopReason !== 'quota') {
1487 $('#resume-banner').slideUp(300);
1488 }
1489
1490 // Update results
1491 $('#result-success').text(state.successCount);
1492 $('#result-saved').text(KingImageOptimizer.formatBytes(state.totalSavedBytes));
1493
1494 const avgPercent = state.successCount > 0
1495 ? Math.round((state.totalSavedBytes / (state.successCount * 500000)) * 100) // Rough estimate
1496 : 0;
1497 $('#result-percent').text(Math.min(avgPercent, 100) + '%');
1498
1499 // Show results
1500 $('#progress-section').slideUp(300);
1501 $('#results-section').slideDown(300);
1502
1503 // Refresh stats
1504 refreshStats();
1505
1506 // Refresh breakdown
1507 refreshBreakdown();
1508 }
1509
1510 /**
1511 * Reset state
1512 */
1513 function resetState() {
1514 state = {
1515 isProcessing: false,
1516 isPaused: false,
1517 shouldStop: false,
1518 stopReason: null,
1519 imageQueue: [],
1520 imageIndex: {},
1521 currentIndex: 0,
1522 totalImages: 0,
1523 successCount: 0,
1524 skippedCount: 0,
1525 errorCount: 0,
1526 failedBase: null,
1527 totalSavedBytes: 0,
1528 startTime: 0,
1529 results: []
1530 };
1531 }
1532
1533 /**
1534 * Show error message
1535 */
1536 function showError(message) {
1537 setUnloadGuard(false);
1538 alert(message); // Simple alert for now, can be enhanced with custom modal
1539 $('#progress-section').hide();
1540 $('#optimization-options').show();
1541 }
1542
1543 /**
1544 * Refresh stats from server
1545 */
1546 function refreshStats() {
1547 $.ajax({
1548 url: kingImageOptimizer.ajaxUrl,
1549 type: 'POST',
1550 data: {
1551 action: 'king_img_get_stats',
1552 nonce: kingImageOptimizer.nonce
1553 },
1554 success: function(response) {
1555 if (response.success) {
1556 $('#stat-total').text(response.data.total_images.toLocaleString());
1557 $('#stat-optimized').text(response.data.optimized_images.toLocaleString());
1558 const skipped = response.data.skipped_images ? response.data.skipped_images : 0;
1559 const failed = response.data.failed_images ? response.data.failed_images : 0;
1560
1561 // Keep the baseline in sync so live display doesn't double count after refreshStats()
1562 state.failedBase = Math.max(0, (parseInt(failed, 10) || 0) - (state.errorCount || 0));
1563
1564 if ($('#stat-failed').length) {
1565 $('#stat-failed').text(failed.toLocaleString());
1566 }
1567 const pending = (typeof response.data.pending_images !== 'undefined')
1568 ? response.data.pending_images
1569 : (response.data.total_images - response.data.optimized_images - skipped - failed);
1570 $('#stat-pending').text(Math.max(0, pending).toLocaleString());
1571 $('#stat-saved').text(KingImageOptimizer.formatBytes(response.data.total_saved_bytes));
1572 }
1573 }
1574 });
1575 }
1576
1577 /**
1578 * Refresh the "Image Library Breakdown" block dynamically.
1579 */
1580 function refreshBreakdown() {
1581 const $list = $('#ka-img-opt-format-list');
1582 if ($list.length === 0) {
1583 return;
1584 }
1585
1586 $.ajax({
1587 url: kingImageOptimizer.ajaxUrl,
1588 type: 'POST',
1589 data: {
1590 action: 'king_img_get_breakdown',
1591 nonce: kingImageOptimizer.nonce
1592 },
1593 success: function(response) {
1594 if (!response.success || !response.data) {
1595 return;
1596 }
1597
1598 const total = parseInt(response.data.total_images || 0, 10) || 0;
1599 const formats = response.data.formats || {};
1600
1601 const keys = Object.keys(formats);
1602 if (keys.length === 0) {
1603 $list.html('');
1604 return;
1605 }
1606
1607 let html = '';
1608 keys.forEach(function(format) {
1609 const count = parseInt(formats[format] || 0, 10) || 0;
1610 const width = total > 0 ? Math.min(100, (count / total) * 100) : 0;
1611
1612 html += '<div class="ka-img-opt-format-item">'
1613 + '<div class="ka-img-opt-format-info">'
1614 + '<span class="ka-img-opt-format-name">' + String(format).toUpperCase() + '</span>'
1615 + '<span class="ka-img-opt-format-count">' + count.toLocaleString() + '</span>'
1616 + '</div>'
1617 + '<div class="ka-img-opt-format-bar">'
1618 + '<div class="ka-img-opt-format-bar-fill" style="width: ' + width + '%;"></div>'
1619 + '</div>'
1620 + '</div>';
1621 });
1622
1623 $list.html(html);
1624 }
1625 });
1626 }
1627
1628 /**
1629 * Save settings
1630 */
1631 function saveSettings() {
1632 const resizeEnabled = $('#settings-resize-enabled').length
1633 ? $('#settings-resize-enabled').is(':checked')
1634 : $('#resize-enabled').is(':checked');
1635
1636 const maxWidth = $('#settings-max-width').length
1637 ? $('#settings-max-width').val()
1638 : $('#max-width').val();
1639
1640 const settingsData = {
1641 action: 'king_img_save_settings',
1642 nonce: kingImageOptimizer.nonce,
1643 quality: $('#settings-quality').val(),
1644 skip_small: $('#settings-skip-small').is(':checked') ? 1 : 0,
1645 auto_replace_urls: $('#settings-auto-replace').is(':checked') ? 1 : 0,
1646 auto_optimize_uploads: $('#settings-auto-optimize-uploads').is(':checked') ? 1 : 0,
1647 resize_enabled: resizeEnabled ? 1 : 0,
1648 max_width: maxWidth
1649 };
1650
1651 $('#save-settings').prop('disabled', true).text('Saving...');
1652
1653 $.ajax({
1654 url: kingImageOptimizer.ajaxUrl,
1655 type: 'POST',
1656 data: settingsData,
1657 success: function(response) {
1658 $('#save-settings').prop('disabled', false).html('<span class="dashicons dashicons-saved"></span> Save Settings');
1659
1660 if (response.success) {
1661 // Show success feedback
1662 $('#save-settings').addClass('saved').text('✓ Saved!');
1663 setTimeout(function() {
1664 $('#save-settings').removeClass('saved').html('<span class="dashicons dashicons-saved"></span> Save Settings');
1665 }, 2000);
1666 }
1667 },
1668 error: function() {
1669 $('#save-settings').prop('disabled', false).html('<span class="dashicons dashicons-saved"></span> Save Settings');
1670 alert('Failed to save settings');
1671 }
1672 });
1673 }
1674
1675 /**
1676 * Restore all images
1677 */
1678 let restoreQueue = [];
1679 let restoreTotal = 0;
1680 let restoreSuccess = 0;
1681 let restoreErrors = 0;
1682 let isRestoring = false;
1683 let restoreLastId = 0;
1684
1685 function restoreAllImages() {
1686 if (!confirm(kingImageOptimizer.strings.confirmBulkRestore || 'Are you sure you want to restore ALL optimized images to originals? This will delete all optimized files.')) {
1687 return;
1688 }
1689
1690 const $btn = $('#restore-all');
1691 $btn.prop('disabled', true).html('<span class="ka-btn-spinner"></span> Loading...');
1692
1693 // First get all optimized image IDs
1694 $.ajax({
1695 url: kingImageOptimizer.ajaxUrl,
1696 type: 'POST',
1697 data: {
1698 action: 'king_img_get_optimized_ids',
1699 nonce: kingImageOptimizer.nonce,
1700 },
1701 success: function(response) {
1702 if (response.success && response.data.ids.length > 0) {
1703 restoreQueue = response.data.ids;
1704 restoreTotal = response.data.total;
1705 restoreSuccess = 0;
1706 restoreErrors = 0;
1707 isRestoring = true;
1708 restoreLastId = 0;
1709
1710 // Show spinner for the entire restore progress
1711 $btn.prop('disabled', true).html('<span class="ka-btn-spinner"></span> Restoring...');
1712
1713 setUnloadGuard(true, (typeof kingImageOptimizer !== 'undefined' && kingImageOptimizer.strings && kingImageOptimizer.strings.leaveWarning)
1714 ? kingImageOptimizer.strings.leaveWarning
1715 : 'Restore is running. Are you sure you want to leave this page?');
1716
1717 // Show restore progress UI
1718 showRestoreProgress();
1719 processNextRestore();
1720 } else {
1721 $btn.prop('disabled', false).html('<span class="dashicons dashicons-undo"></span> Restore All Originals');
1722 setUnloadGuard(false);
1723 if (response.data.total === 0) {
1724 showNotification('No optimized images found to restore.', 'info');
1725 }
1726 }
1727 },
1728 error: function() {
1729 $btn.prop('disabled', false).html('<span class="dashicons dashicons-undo"></span> Restore All Originals');
1730 setUnloadGuard(false);
1731 showNotification('Failed to get optimized images list.', 'error');
1732 }
1733 });
1734 }
1735
1736 function showRestoreProgress() {
1737 $('#restore-all-progress').show();
1738 $('#restore-current-filename').text('Preparing...');
1739 updateRestoreProgress();
1740 }
1741
1742 function updateRestoreProgress() {
1743 const processed = restoreSuccess + restoreErrors;
1744 const percent = restoreTotal > 0 ? Math.round((processed / restoreTotal) * 100) : 0;
1745
1746 $('#restore-progress-fill').css('width', percent + '%');
1747 $('#restore-progress-percent').text(percent + '%');
1748 $('#restore-progress-count').text(processed + ' / ' + restoreTotal);
1749
1750 if (restoreLastId) {
1751 $('#restore-current-filename').text('Restoring media id ' + restoreLastId + '...');
1752 } else {
1753 $('#restore-current-filename').text('Restoring...');
1754 }
1755 }
1756
1757 function processNextRestore() {
1758 if (!isRestoring || restoreQueue.length === 0) {
1759 finishBulkRestore();
1760 return;
1761 }
1762
1763 const attachmentId = restoreQueue.shift();
1764 restoreLastId = attachmentId;
1765 updateRestoreProgress();
1766
1767 $.ajax({
1768 url: kingImageOptimizer.ajaxUrl,
1769 type: 'POST',
1770 data: {
1771 action: 'king_img_bulk_restore_single',
1772 nonce: kingImageOptimizer.nonce,
1773 attachment_id: attachmentId,
1774 },
1775 success: function(response) {
1776 if (response.success) {
1777 restoreSuccess++;
1778 } else {
1779 restoreErrors++;
1780 }
1781 updateRestoreProgress();
1782 processNextRestore();
1783 },
1784 error: function() {
1785 restoreErrors++;
1786 updateRestoreProgress();
1787 processNextRestore();
1788 }
1789 });
1790 }
1791
1792 function finishBulkRestore() {
1793 isRestoring = false;
1794 restoreLastId = 0;
1795 setUnloadGuard(false);
1796 updateRestoreProgress();
1797
1798 // Show a clear completion state
1799 $('#restore-current-filename').text('Done');
1800
1801 const $btn = $('#restore-all');
1802 $btn.prop('disabled', true).html('<span class="dashicons dashicons-yes-alt"></span> Restore Complete');
1803
1804 // Show completion notification
1805 showNotification(
1806 `Restore complete! ${restoreSuccess} images restored, ${restoreErrors} errors.`,
1807 restoreErrors > 0 ? 'warning' : 'success'
1808 );
1809 }
1810
1811 // Initialize when DOM is ready
1812 $(document).ready(function() {
1813 init();
1814 });
1815
1816 })(jQuery);
1817