diff --git a/src/class-tiny-background-optimize.php b/src/class-tiny-background-optimize.php new file mode 100644 index 00000000..afcc689f --- /dev/null +++ b/src/class-tiny-background-optimize.php @@ -0,0 +1,382 @@ +settings = $settings; + } + + public function ajax_init() { + add_action( 'wp_ajax_' . self::WORKER_ACTION, $this->get_method( 'work' ) ); + } + + /** + * Queue attachments and start working through them. + * + * Whatever an earlier run left behind is forgotten first. + * + * @param int[] $ids Attachments to optimize. + */ + public function start( array $ids ) { + delete_post_meta_by_key( self::META_KEY_STATUS ); + delete_post_meta_by_key( self::META_KEY_RESULT ); + + $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) ); + foreach ( $ids as $id ) { + add_post_meta( $id, self::META_KEY_STATUS, self::STATUS_QUEUED, true ); + } + + // cleanup workers + for ( $worker = 1; $worker <= self::WORKERS; $worker++ ) { + delete_transient( self::WORKER_TRANSIENT . $worker ); + } + + $this->start_workers(); + } + + /** + * Take every queued attachment off the queue. Attachments that are being + * optimized right now still finish. + */ + public function cancel() { + delete_metadata( 'post', 0, self::META_KEY_STATUS, self::STATUS_QUEUED, true ); + } + + /** + * Whether something is still being processed + * - any worker is active + * - any image is queued or processing + * + * @return bool + */ + public function is_running() { + return $this->has_active_workers() && ( $this->next_queued() || $this->get_processing() ); + } + + /** + * Where the given attachments stand in the current run. + * + * @param int[] $ids Attachment IDs. + * @return array Keyed by + * attachment ID. The status is null when the attachment is not part + * of the run; the result is only there once it is done or failed. + */ + public function get_results( array $ids ) { + $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) ); + + update_meta_cache( 'post', $ids ); + + $results = array(); + foreach ( $ids as $id ) { + $status = get_post_meta( $id, self::META_KEY_STATUS, true ); + $finished = self::STATUS_DONE === $status || self::STATUS_FAILED === $status; + + $result = $finished ? get_post_meta( $id, self::META_KEY_RESULT, true ) : null; + + $results[ $id ] = array( + 'status' => '' === $status ? null : $status, + 'result' => $finished ? (array) $result : null, + ); + } + + return $results; + } + + /** + * Optimize one queued attachment, then send this worker's next request and + * start any worker that died. + * + * Sending the next request only once this one is done keeps the number of + * workers at WORKERS. + */ + public function work() { + Tiny_Logger::debug( 'received start work' ); + check_ajax_referer( self::WORKER_ACTION, 'nonce' ); + + ignore_user_abort( true ); + + $worker = isset( $_POST['worker'] ) ? intval( $_POST['worker'] ) : 0; + + $id = $this->next_queued(); + if ( $id ) { + Tiny_Logger::debug( + 'worker starting task', + array( + 'attachment_id' => $id, + 'worker_nr' => $worker, + ) + ); + $this->task( $id ); + $this->start_worker( $worker ); + $this->start_workers(); + } else { + delete_transient( self::WORKER_TRANSIENT . $worker ); + } + + wp_die(); + } + + /** + * Start every worker that is not active. + */ + private function start_workers() { + $inactive = array(); + for ( $worker = 1; $worker <= self::WORKERS; $worker++ ) { + $is_active = get_transient( self::WORKER_TRANSIENT . $worker ); + if ( ! $is_active ) { + set_transient( self::WORKER_TRANSIENT . $worker, time(), self::STALLED_AFTER ); + $inactive[] = $worker; + } + } + + foreach ( $inactive as $worker ) { + Tiny_Logger::debug( + 'worker stalled', + array( + 'worker_nr' => $worker, + ) + ); + $this->start_worker( $worker ); + } + } + + private function has_active_workers() { + for ( $worker = 1; $worker <= self::WORKERS; $worker++ ) { + $is_active = get_transient( self::WORKER_TRANSIENT . $worker ); + if ( $is_active ) { + return true; + } + } + return false; + } + + /** + * Send the next request for a worker, and mark it active until then. + * + * The worker runs as the user whose request starts it. + * + * @param int $worker Worker number, from 1 to WORKERS. + */ + private function start_worker( $worker ) { + Tiny_Logger::debug( + 'starting worker', + array( + 'worker_nr' => $worker, + ) + ); + set_transient( self::WORKER_TRANSIENT . $worker, time(), self::STALLED_AFTER ); + $args = array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => array( + 'action' => self::WORKER_ACTION, + 'nonce' => wp_create_nonce( self::WORKER_ACTION ), + 'worker' => $worker, + ), + 'cookies' => isset( $_COOKIE ) && is_array( $_COOKIE ) ? $_COOKIE : array(), + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + + $remote_request_result = null; + if ( getenv( 'WORDPRESS_HOST' ) !== false ) { + $remote_request_result = wp_remote_post( + getenv( 'WORDPRESS_HOST' ) . '/wp-admin/admin-ajax.php', + $args + ); + } else { + $remote_request_result = wp_remote_post( + admin_url( 'admin-ajax.php' ), + $args + ); + } + + if ( is_wp_error( $remote_request_result ) ) { + Tiny_Logger::error( + 'unable to send remote request', + array( + 'worker_number' => $worker, + 'error' => $remote_request_result->get_error_message(), + ) + ); + } + } + + /** + * @return int|null The next queued attachment, if any. + */ + private function next_queued() { + global $wpdb; + + $id = $wpdb->get_var( + $wpdb->prepare( + "SELECT post_id FROM $wpdb->postmeta + WHERE meta_key = %s AND meta_value = %s + ORDER BY post_id DESC + LIMIT 1", + self::META_KEY_STATUS, + self::STATUS_QUEUED + ) + ); + + return is_null( $id ) ? null : intval( $id ); + } + + /** + * @return int[] Attachments being optimized. + */ + private function get_processing() { + global $wpdb; + + return array_map( + 'intval', + $wpdb->get_col( + $wpdb->prepare( + "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s", + self::META_KEY_STATUS, + self::STATUS_PROCESSING + ) + ) + ); + } + + /** + * Optimize a queued attachment. + * + * The attachment always leaves the queue, whatever happens: one that stayed + * queued would be handed out again and again. + * + * @param int $id Attachment ID. + */ + private function task( $id ) { + $claimed = update_post_meta( + $id, + self::META_KEY_STATUS, + self::STATUS_PROCESSING, + self::STATUS_QUEUED + ); + + if ( ! $claimed ) { + return; + } + + try { + $result = $this->compress( $id ); + $status = $result['failed'] > 0 ? self::STATUS_FAILED : self::STATUS_DONE; + } catch ( Exception $e ) { + $result = array( + 'success' => 0, + 'failed' => 1, + 'message' => $e->getMessage(), + 'size_change' => 0, + ); + $status = self::STATUS_FAILED; + } + + update_post_meta( $id, self::META_KEY_RESULT, $result ); + update_post_meta( $id, self::META_KEY_STATUS, $status ); + + $compressor = $this->settings->get_compressor(); + if ( $compressor && $compressor->limit_reached() ) { + $this->cancel(); + } + } + + /** + * Compress an attachment and say what that came to. + * + * @param int $id Attachment ID. + * @return array{success: int, failed: int, message: string|null, size_change: int} + */ + private function compress( $id ) { + $active_sizes = $this->settings->get_sizes(); + $active_tinify_sizes = $this->settings->get_active_tinify_sizes(); + + $before = new Tiny_Image( $this->settings, $id ); + $before = $before->get_statistics( $active_sizes, $active_tinify_sizes ); + + $tiny_image = new Tiny_Image( $this->settings, $id ); + + Tiny_Logger::debug( + 'compress from background queue', + array( + 'image_id' => $id, + ) + ); + + $result = $tiny_image->compress(); + wp_update_attachment_metadata( $id, $tiny_image->get_wp_metadata() ); + + $after = $tiny_image->get_statistics( $active_sizes, $active_tinify_sizes ); + + return array( + 'success' => isset( $result['success'] ) ? intval( $result['success'] ) : 0, + 'failed' => isset( $result['failed'] ) ? intval( $result['failed'] ) : 0, + 'message' => $tiny_image->get_latest_error(), + 'size_change' => $after['compressed_total_size'] - $before['compressed_total_size'], + ); + } +} diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 735d8328..508d2bfc 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -22,10 +22,15 @@ class Tiny_Plugin extends Tiny_WP_Base { const MEDIA_COLUMN = self::NAME; const DATETIME_FORMAT = 'Y-m-d G:i:s'; - private static $version; - + /** @var Tiny_Settings */ private $settings; - private $twig; + + /** + * Background process + * + * @var Tiny_Background_Optimize + */ + private $background_optimizer; public static function jpeg_quality() { return 85; @@ -42,6 +47,7 @@ public function __construct() { parent::__construct(); $this->settings = new Tiny_Settings(); new Tiny_Conversion( $this->settings ); + $this->background_optimizer = new Tiny_Background_Optimize( $this->settings ); } public function set_compressor( $compressor ) { @@ -100,8 +106,18 @@ public function ajax_init() { ); add_action( - 'wp_ajax_tiny_compress_image_for_bulk', - $this->get_method( 'compress_image_for_bulk' ) + 'wp_ajax_tiny_bulk_queue_start', + $this->get_method( 'ajax_bulk_queue_start' ) + ); + + add_action( + 'wp_ajax_tiny_bulk_queue_status', + $this->get_method( 'ajax_bulk_queue_status' ) + ); + + add_action( + 'wp_ajax_tiny_bulk_queue_cancel', + $this->get_method( 'ajax_bulk_queue_cancel' ) ); add_action( @@ -304,16 +320,13 @@ public function enqueue_scripts( $hook ) { 'No action taken', 'tiny-compress-images' ), - 'L10nDuplicate' => __( - 'Image was already processed', - 'tiny-compress-images' - ), 'L10nBulkAction' => __( 'Compress Images', 'tiny-compress-images' ), 'L10nBulkMarkCompressed' => __( 'Mark as Compressed', 'tiny-compress-images' ), 'L10nCancelled' => __( 'Cancelled', 'tiny-compress-images' ), + 'L10nStopped' => __( 'Stopped', 'tiny-compress-images' ), 'L10nCompressing' => __( 'Compressing', 'tiny-compress-images' ), 'L10nCompressed' => __( 'compressed', 'tiny-compress-images' ), 'L10nConverted' => __( 'converted', 'tiny-compress-images' ), @@ -335,6 +348,15 @@ public function enqueue_scripts( $hook ) { 'L10nInternalError' => __( 'Internal error', 'tiny-compress-images' ), 'L10nOutOf' => __( 'out of', 'tiny-compress-images' ), 'L10nWaiting' => __( 'Waiting', 'tiny-compress-images' ), + 'L10nBackgroundNotice' => __( + // phpcs:ignore Generic.Files.LineLength + 'Optimization continues in the background. You can close this page and come back later.', + 'tiny-compress-images' + ), + 'L10nStoppedNotice' => __( + 'Optimization stopped early.', + 'tiny-compress-images' + ), ) ); @@ -594,47 +616,21 @@ public function compress_image_from_library() { exit(); } - public function compress_image_for_bulk() { - $response = $this->validate_ajax_attachment_request(); - if ( isset( $response['error'] ) ) { - echo json_encode( $response ); - exit(); - } - - list($id, $metadata) = $response['data']; - $tiny_image_before = new Tiny_Image( $this->settings, $id, $metadata ); - $image_statistics_before = $tiny_image_before->get_statistics( - $this->settings->get_sizes(), - $this->settings->get_active_tinify_sizes() - ); - $size_before = $image_statistics_before['compressed_total_size']; - - $tiny_image = new Tiny_Image( $this->settings, $id, $metadata ); - - Tiny_Logger::debug( - 'compress from bulk', - array( - 'image_id' => $id, - ) - ); - - $result = $tiny_image->compress(); + /** + * What the bulk optimization page shows about an image once it is done. + * + * @param Tiny_Image $tiny_image The image as it is after compressing. + * @param array $result What compressing it came to: at least + * 'success', 'failed', 'message' and + * 'size_change'. + * @return array + */ + private function bulk_image_result( $tiny_image, array $result ) { $image_statistics = $tiny_image->get_statistics( $this->settings->get_sizes(), $this->settings->get_active_tinify_sizes() ); - wp_update_attachment_metadata( $id, $tiny_image->get_wp_metadata() ); - // Nonce verified in validate_ajax_attachment_request(). - // phpcs:disable WordPress.Security.NonceVerification.Missing - $current_library_size = isset( $_POST['current_size'] ) ? - intval( wp_unslash( $_POST['current_size'] ) ) - : 0; - // phpcs:enable WordPress.Security.NonceVerification.Missing - $size_after = $image_statistics['compressed_total_size']; - $new_library_size = $current_library_size + $size_after - $size_before; - - $result['message'] = $tiny_image->get_latest_error(); $result['image_sizes_compressed'] = $image_statistics['image_sizes_compressed']; $result['image_sizes_converted'] = $image_statistics['image_sizes_converted']; $result['image_sizes_optimized'] = $image_statistics['image_sizes_optimized']; @@ -649,10 +645,10 @@ public function compress_image_for_bulk() { 1 ); - $result['savings'] = $tiny_image->get_savings( $image_statistics ); - $result['status'] = $this->settings->get_status(); - $result['thumbnail'] = wp_get_attachment_image( - $id, + $result['savings'] = $tiny_image->get_savings( $image_statistics ); + $result['status'] = $this->settings->get_status(); + $result['thumbnail'] = wp_get_attachment_image( + $tiny_image->get_id(), array( '30', '30' ), true, array( @@ -660,12 +656,95 @@ public function compress_image_for_bulk() { 'alt' => '', ) ); - $result['size_change'] = $size_after - $size_before; - $result['human_readable_library_size'] = size_format( $new_library_size, 2 ); - echo json_encode( $result ); + return $result; + } + + /** + * Retrieves images available for optimization and + * queues them for optimization + */ + public function ajax_bulk_queue_start() { + check_ajax_referer( 'tiny-compress', '_nonce' ); + if ( ! current_user_can( 'upload_files' ) ) { + wp_die( -1, 403 ); + } + + /* A run that is still going is followed, not restarted. */ + if ( $this->background_optimizer->is_running() ) { + wp_send_json_success(); + } + + $stats = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); + $ids = wp_list_pluck( $stats['available-for-optimization'], 'ID' ); + $this->background_optimizer->start( $ids ); + + wp_send_json_success(); + } + + /** + * Where the images the bulk optimization page is waiting for stand. + * + * Finished images come with the same details the page gets when it + * compresses an image itself, so it can show them the same way. + */ + public function ajax_bulk_queue_status() { + check_ajax_referer( 'tiny-compress', '_nonce' ); + if ( ! current_user_can( 'upload_files' ) ) { + wp_die( -1, 403 ); + } + + $ids = isset( $_POST['ids'] ) ? + explode( ',', sanitize_text_field( wp_unslash( $_POST['ids'] ) ) ) + : array(); + $library_size = isset( $_POST['current_size'] ) ? + intval( wp_unslash( $_POST['current_size'] ) ) + : 0; + + $running = $this->background_optimizer->is_running(); + + $items = array(); + foreach ( $this->background_optimizer->get_results( $ids ) as $id => $item ) { + if ( is_array( $item['result'] ) ) { + $item['result'] = $this->bulk_image_result( + new Tiny_Image( $this->settings, $id ), + array_merge( + array( + 'success' => 0, + 'failed' => 0, + 'message' => null, + 'size_change' => 0, + ), + $item['result'] + ) + ); + + /* The page applies the results in order, each on top of the last. */ + $library_size += $item['result']['size_change']; + + $item['result']['human_readable_library_size'] = size_format( $library_size, 2 ); + } + + $items[ $id ] = $item; + } + + wp_send_json_success( + array( + 'running' => $running, + 'items' => $items, + ) + ); + } + + public function ajax_bulk_queue_cancel() { + check_ajax_referer( 'tiny-compress', '_nonce' ); + if ( ! current_user_can( 'upload_files' ) ) { + wp_die( -1, 403 ); + } - exit(); + $this->background_optimizer->cancel(); + + wp_send_json_success(); } public function ajax_optimization_statistics() { @@ -815,6 +894,7 @@ public function render_bulk_optimization_page() { $remaining_credits = $this->settings->get_remaining_credits(); $is_on_free_plan = $this->settings->is_on_free_plan(); $email_address = $this->settings->get_email_address(); + $bulk_running = $this->background_optimizer->is_running(); include __DIR__ . '/views/bulk-optimization.php'; } diff --git a/src/js/bulk-optimization.js b/src/js/bulk-optimization.js index 4f4fd6a1..a0234bec 100644 --- a/src/js/bulk-optimization.js +++ b/src/js/bulk-optimization.js @@ -1,6 +1,8 @@ (function() { - const processedItems = []; - var parallelCompressions = 5; + const POLL_INTERVAL = 2000; + const POLL_WINDOW = 10; + let pending = []; + let stoppedEarly = false; function updateProgressBar(successFullCompressions) { var totalToOptimize = parseInt(jQuery('div#compression-progress-bar').data('number-to-optimize'), 10); @@ -68,30 +70,10 @@ } } - function bulkOptimizationCallback(error, data, items, i) { - if (window.optimizationCancelled) { - handleCancellation(); - } - + function bulkOptimizationCallback(data, items, i) { var row = jQuery('#optimization-items tr').eq(parseInt(i, 10)+1); - if (error) { - row.addClass('failed'); - row.find('.status').html(tinyCompress.L10nInternalError + '
' + error.toString()); - row.find('.status').attr('title', error.toString()); - row.find('.status').attr('data-status', 'error'); - data = {}; - } else if (data == null) { - row.addClass('failed'); - row.find('.status').html(tinyCompress.L10nError); - row.find('.status').attr('data-status', 'error'); - data = {}; - } else if (data.error) { - row.addClass('failed'); - row.find('.status').html(tinyCompress.L10nError + '
' + data.error); - row.find('.status').attr('title', data.error); - row.find('.status').attr('data-status', 'error'); - } else if (data.failed > 0) { + if (data.failed > 0) { row.addClass('failed'); row.find('.status').html('' + tinyCompress.L10nLatestError + ': ' + data.message + ''); row.find('.status').attr('title', data.message); @@ -122,88 +104,106 @@ row.find('.initial-size').html(data.initial_total_size); row.find('.optimized-size').html(data.optimized_total_size); row.find('.savings').html(data.savings); - - var totalToOptimize = jQuery('td.status[data-status="waiting"],td.status[data-status="compressing"]').length; - var nextImage; - - if (jQuery('td.status[data-status="waiting"]').length > 0) { - nextImage = jQuery('tr.media-item').index(jQuery('td.status[data-status="waiting"]:first').parents('tr')); - } - if (nextImage !== undefined && items[nextImage]) { - if (!window.optimizationCancelled) { - drawSomeRows(items, 1); - } - bulkOptimizeItem(items, nextImage); - } else if (totalToOptimize === 0) { - var message = jQuery('

'); - message.find('p').html(tinyCompress.L10nAllDone); - message.insertAfter(jQuery('#tiny-bulk-optimization h1')); - jQuery('div#optimization-spinner').css('display', 'none'); - jQuery('div#bulk-optimization-actions').hide(); - jQuery('div.progress').css('animation', 'none'); - } } - function bulkOptimizeItem(items, i) { - if (window.optimizationCancelled) { - return; - } - - const itemID = items[i].ID; - if (processedItems.includes(itemID)) { - const row = jQuery('#optimization-items tr').eq(parseInt(i, 10) + 1); - row.find('.status') - .attr('data-status', 'skipped-duplicate') - .html('' + tinyCompress.L10nDuplicate); + function finishOptimization(message) { + if (message) { + const notice = jQuery('

'); + notice.find('p').text(message); + notice.insertAfter(jQuery('#tiny-bulk-optimization h2')); } + jQuery('div#optimization-spinner').css('display', 'none'); + handleCancellation(); + } - var row = jQuery('#optimization-items tr').eq(parseInt(i, 10)+1); - row.find('.status').removeClass('todo'); - row.find('.status').html('' + tinyCompress.L10nCompressing).attr('data-status', 'compressing'); - jQuery.ajax({ - url: ajaxurl, - type: 'POST', - dataType: 'json', - data: { - _nonce: tinyCompress.nonce, - action: 'tiny_compress_image_for_bulk', - id: itemID, - current_size: window.currentLibraryBytes - }, - success: function(data) { bulkOptimizationCallback(null, data, items, i); }, - error: function(xhr, textStatus, errorThrown) { bulkOptimizationCallback(errorThrown, null, items, i, parallelCompressions); } + // The queue works through the images in the order they are listed, so only + // the first few waiting ones need asking about. + function pollStatus(items) { + const batch = pending.slice(0, POLL_WINDOW); + drawSomeRows(items, batch[batch.length - 1] + 1); + jQuery.post(ajaxurl, { + _nonce: tinyCompress.nonce, + action: 'tiny_bulk_queue_status', + ids: batch.map(function(i) { return items[i].ID; }).join(','), + current_size: window.currentLibraryBytes + }, function(response) { + const data = response.data; + let finished = 0; + const waiting = batch.filter(function(i) { + const item = data.items[items[i].ID] || {}; + if (item.status === 'done' || item.status === 'failed') { + bulkOptimizationCallback(item.result, items, i); + finished++; + } else if (data.running && (item.status === 'queued' || item.status === 'processing')) { + return true; + } else { + // Unless this page cancelled, the run ended because every worker stopped. + const label = window.optimizationCancelled ? tinyCompress.L10nCancelled : tinyCompress.L10nStopped; + jQuery('#optimization-items tr').eq(i + 1).find('.status').html(label).attr('data-status', 'cancelled'); + stoppedEarly = true; + } + return false; + }); + pending = waiting.concat(pending.slice(batch.length)); + + if (pending.length > 0 && (data.running || finished > 0)) { + setTimeout(pollStatus, POLL_INTERVAL, items); + } else if (window.optimizationCancelled) { + finishOptimization(null); + } else { + finishOptimization(stoppedEarly ? tinyCompress.L10nStoppedNotice : tinyCompress.L10nAllDone); + } + }, 'json').fail(function(xhr) { + if (xhr.status === 403) { + finishOptimization(tinyCompress.L10nInternalError); + } else { + setTimeout(pollStatus, POLL_INTERVAL, items); + } }); - processedItems.push(itemID); - jQuery('#tiny-progress span').html(i + 1); } - function prepareBulkOptimization(items) { + function prepareBulkOptimization(items, running) { window.allBulkOptimizationItems = items; updateProgressBar(0); + if (running) { + startBulkOptimization(items, true); + } } - function startBulkOptimization(items) { + function startBulkOptimization(items, running) { window.optimizationCancelled = false; window.totalRowsDrawn = 0; window.currentLibraryBytes = parseInt(jQuery('#optimized-library-size').data('bytes'), 10); - processedItems.splice(0, processedItems.length); + pending = items.map(function(item, i) { return i; }); + stoppedEarly = false; + jQuery('div#bulk-optimization-actions input').removeClass('visible'); + jQuery('div#bulk-optimization-actions p.optimization-buttons_notice').text(tinyCompress.L10nBackgroundNotice); jQuery('div.progress').css('animation', 'progress-bar 80s linear infinite'); jQuery('div#optimization-spinner').css('display', 'inline-block'); updateProgressBar(0); - drawSomeRows(items, 5 + parallelCompressions); - for (var i = 0; i < parallelCompressions; i++) { - if (items.length >= i+1) { - bulkOptimizeItem(items, i); - } + if (running) { + jQuery('div#bulk-optimization-actions input#id-optimizing').addClass('visible'); + pollStatus(items); + return; } + + jQuery.post(ajaxurl, { + _nonce: tinyCompress.nonce, + action: 'tiny_bulk_queue_start' + }, function() { + jQuery('div#bulk-optimization-actions input#id-optimizing').addClass('visible'); + pollStatus(items); + }, 'json').fail(function() { + finishOptimization(tinyCompress.L10nInternalError); + }); } - function drawSomeRows(items, rowsToDraw) { + function drawSomeRows(items, end) { const list = jQuery('#optimization-items tbody'); const start = window.totalRowsDrawn; - const end = Math.min(start + rowsToDraw, items.length); + end = Math.min(end, items.length); for (let i = start; i < end; i++) { const tableRow = ` @@ -215,15 +215,27 @@ `; list.append(tableRow); } - window.totalRowsDrawn = end; + window.totalRowsDrawn = Math.max(start, end); } - function cancelOptimization() { - window.optimizationCancelled = true; - jQuery('div#optimization-spinner').css('display', 'none'); - jQuery(jQuery('#optimization-items tr td.status.todo')).html(tinyCompress.L10nCancelled).attr('data-status', 'cancelled'); - jQuery('div#bulk-optimization-actions input').removeClass('visible'); - jQuery('div#bulk-optimization-actions input#id-cancelling').addClass('visible'); + async function cancelOptimization() { + try { + window.optimizationCancelled = true; + jQuery('div#optimization-spinner').css('display', 'none'); + jQuery('div#bulk-optimization-actions input').removeClass('visible'); + jQuery('div#bulk-optimization-actions input#id-cancelling').addClass('visible'); + await jQuery.post(ajaxurl, { + _nonce: tinyCompress.nonce, + action: 'tiny_bulk_queue_cancel', + }); + } catch (err) { + // Cancel failed, revert state + window.optimizationCancelled = false; + jQuery('div#optimization-spinner').css('display', 'inline-block'); + jQuery('div#bulk-optimization-actions input').removeClass('visible'); + jQuery('div#bulk-optimization-actions input#id-optimizing').addClass('visible'); + jQuery('div#bulk-optimization-actions p.optimization-buttons_notice').text(tinyCompress.L10nInternalError); + } } jQuery('.tiny-bulk-optimization .upgrade-account-notice a#hide-warning').click(function() { @@ -233,8 +245,6 @@ jQuery('div#bulk-optimization-actions input').click(function() { if ((jQuery(this).attr('id') === 'id-start') && jQuery(this).hasClass('visible')) { - jQuery('div#bulk-optimization-actions input#id-start').removeClass('visible'); - jQuery('div#bulk-optimization-actions input#id-optimizing').addClass('visible'); startBulkOptimization(window.allBulkOptimizationItems); } if ((jQuery(this).attr('id') === 'id-cancel') && jQuery(this).hasClass('visible')) { diff --git a/src/views/bulk-optimization-form.php b/src/views/bulk-optimization-form.php index 2373d63d..e3e8c119 100644 --- a/src/views/bulk-optimization-form.php +++ b/src/views/bulk-optimization-form.php @@ -8,5 +8,5 @@ submit_button( esc_attr__( 'Cancelling', 'tiny-compress-images' ) . '...', 'button-primary button-hero red', 'id-cancelling', false ); ?>
-

+

diff --git a/src/views/bulk-optimization.php b/src/views/bulk-optimization.php index 31ad62dc..3ba98ec4 100644 --- a/src/views/bulk-optimization.php +++ b/src/views/bulk-optimization.php @@ -9,6 +9,7 @@ * @var string $email_address * @var bool $is_on_free_plan * @var int $remaining_credits + * @var bool $bulk_running * @var Tiny_Plugin $this */ @@ -326,7 +327,7 @@ ?> diff --git a/tiny-compress-images.php b/tiny-compress-images.php index 7802daa9..578bc8bb 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -19,6 +19,7 @@ require dirname( __FILE__ ) . '/src/class-tiny-diagnostics.php'; require dirname( __FILE__ ) . '/src/class-tiny-compress.php'; require dirname( __FILE__ ) . '/src/class-tiny-bulk-optimization.php'; +require dirname( __FILE__ ) . '/src/class-tiny-background-optimize.php'; require dirname( __FILE__ ) . '/src/class-tiny-image-size.php'; require dirname( __FILE__ ) . '/src/class-tiny-image.php'; require dirname( __FILE__ ) . '/src/class-tiny-settings.php';