diff --git a/src/class-tiny-bulk-optimization.php b/src/class-tiny-bulk-optimization.php index 611fbf44..10c12811 100644 --- a/src/class-tiny-bulk-optimization.php +++ b/src/class-tiny-bulk-optimization.php @@ -24,14 +24,12 @@ class Tiny_Bulk_Optimization { const PAGING_SIZE = 25000; public static function get_optimization_statistics( $settings, $result = null ) { - $stats = array(); - $stats['uploaded-images'] = 0; - $stats['optimized-image-sizes'] = 0; - $stats['available-unoptimized-sizes'] = 0; - $stats['optimized-library-size'] = 0; - $stats['unoptimized-library-size'] = 0; - $stats['estimated_credit_use'] = 0; - $stats['available-for-optimization'] = array(); + $stats = array(); + $stats['uploaded-images'] = 0; + $stats['optimized-library-size'] = 0; + $stats['unoptimized-library-size'] = 0; + $stats['estimated_credit_use'] = 0; + $stats['available-for-optimization'] = array(); if ( is_null( $result ) ) { $last_image_id = null; @@ -104,6 +102,8 @@ private static function populate_optimization_statistics( $settings, $result, $s $active_sizes = $settings->get_sizes(); $active_tinify_sizes = $settings->get_active_tinify_sizes(); $conversion_enabled = $settings->get_conversion_enabled(); + $uploads = wp_upload_dir(); + $uploads_url = $uploads['baseurl']; $result_count = count( $result ); for ( $i = 0; $i < $result_count; $i++ ) { @@ -125,11 +125,9 @@ private static function populate_optimization_statistics( $settings, $result, $s $image_stats = $tiny_image->get_statistics( $active_sizes, $active_tinify_sizes ); ++$stats['uploaded-images']; - $stats['estimated_credit_use'] += $image_stats['available_uncompressed_sizes']; - $stats['available-unoptimized-sizes'] += $image_stats['available_unoptimized_sizes']; - $stats['optimized-image-sizes'] += $image_stats['image_sizes_optimized']; - $stats['optimized-library-size'] += $image_stats['compressed_total_size']; - $stats['unoptimized-library-size'] += $image_stats['initial_total_size']; + $stats['estimated_credit_use'] += $image_stats['available_uncompressed_sizes']; + $stats['optimized-library-size'] += $image_stats['compressed_total_size']; + $stats['unoptimized-library-size'] += $image_stats['initial_total_size']; if ( $conversion_enabled ) { $stats['estimated_credit_use'] += $image_stats['available_unconverted_sizes']; @@ -144,10 +142,44 @@ private static function populate_optimization_statistics( $settings, $result, $s $stats['available-for-optimization'][] = array( 'ID' => $result[ $i ]['ID'], 'post_title' => $result[ $i ]['post_title'], + 'file' => self::get_file_name( $wp_metadata ), + 'sizes' => $image_stats['available_unoptimized_sizes'], + 'bytes' => $image_stats['compressed_total_size'], + 'thumbnail' => self::get_thumbnail_url( $wp_metadata, $uploads_url ), ); } }// End for(). return $stats; } + + /** + * The name of the file that was uploaded, also when WordPress scaled it down. + */ + private static function get_file_name( $wp_metadata ) { + if ( isset( $wp_metadata['original_image'] ) ) { + return wp_basename( $wp_metadata['original_image'] ); + } + if ( isset( $wp_metadata['file'] ) ) { + return wp_basename( $wp_metadata['file'] ); + } + return ''; + } + + /** + * The URL of the image's thumbnail, built from its metadata so the page + * does not need a query for each image. Falls back to the image itself. + */ + private static function get_thumbnail_url( $wp_metadata, $uploads_url ) { + if ( ! isset( $wp_metadata['file'] ) ) { + return ''; + } + $file = $wp_metadata['file']; + if ( isset( $wp_metadata['sizes']['thumbnail']['file'] ) ) { + $directory = dirname( $file ); + $file = ( '.' === $directory ? '' : $directory . '/' ) . + $wp_metadata['sizes']['thumbnail']['file']; + } + return $uploads_url . '/' . $file; + } } diff --git a/src/class-tiny-compress.php b/src/class-tiny-compress.php index 346eaed6..c60aef76 100644 --- a/src/class-tiny-compress.php +++ b/src/class-tiny-compress.php @@ -40,15 +40,6 @@ public static function create( $api_key, $after_compress_callback = null ) { return new $class( $api_key, $after_compress_callback ); } - /* Based on pricing April 2016. */ - public static function estimate_cost( $compressions, $compressions_used ) { - return round( - self::compression_cost( $compressions + $compressions_used ) - - self::compression_cost( $compressions_used ), - 2 - ); - } - protected function __construct( $after_compress_callback ) { $this->after_compress_callback = $after_compress_callback; } @@ -202,22 +193,4 @@ private static function needs_resize( $file, $resize_options ) { return $should_resize_width || $should_resize_height; } - - private static function compression_cost( $total ) { - $cost = 0; - - if ( $total > 10000 ) { - $compressions = $total - 10000; - $cost += $compressions * 0.002; - $total -= $compressions; - } - - if ( $total > Tiny_Config::MONTHLY_FREE_COMPRESSIONS ) { - $compressions = $total - Tiny_Config::MONTHLY_FREE_COMPRESSIONS; - $cost += $compressions * 0.009; - $total -= $compressions; - } - - return $cost; - } } diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 59539323..0bbdadcb 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -32,6 +32,9 @@ class Tiny_Plugin extends Tiny_WP_Base { */ private $background_optimizer; + /** @var Tiny_Dashboard */ + private $dashboard; + public static function jpeg_quality() { return 85; } @@ -48,7 +51,10 @@ public function __construct() { $this->settings = new Tiny_Settings(); new Tiny_Conversion( $this->settings ); $this->background_optimizer = new Tiny_Background_Optimize( $this->settings ); - new Tiny_Dashboard( $this->settings, $this->background_optimizer ); + $this->dashboard = new Tiny_Dashboard( + $this->settings, + $this->background_optimizer + ); } public function set_compressor( $compressor ) { @@ -285,13 +291,6 @@ public function enqueue_scripts( $hook ) { self::version() ); - wp_enqueue_style( - self::NAME . '_chart', - plugins_url( '/css/optimization-chart.css', __FILE__ ), - array(), - self::version() - ); - wp_register_script( self::NAME . '_admin', plugins_url( '/js/admin.js', __FILE__ ), @@ -308,10 +307,6 @@ public function enqueue_scripts( $hook ) { 'nonce' => wp_create_nonce( 'tiny-compress' ), 'wpVersion' => self::wp_version(), 'pluginVersion' => self::version(), - 'L10nAllDone' => __( - 'All images are processed', - 'tiny-compress-images' - ), 'L10nNoActionTaken' => __( 'No action taken', 'tiny-compress-images' @@ -321,38 +316,52 @@ public function enqueue_scripts( $hook ) { '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' ), - 'L10nFile' => __( 'File', 'tiny-compress-images' ), - 'L10nSizesOptimized' => __( - 'Sizes optimized', + 'L10nWaiting' => __( 'Waiting', 'tiny-compress-images' ), + 'L10nCompressing' => __( 'Compressing...', 'tiny-compress-images' ), + 'L10nPause' => __( 'Pause', 'tiny-compress-images' ), + 'L10nPausing' => __( 'Pausing...', 'tiny-compress-images' ), + /* translators: %s: number of image sizes */ + 'L10nSizeCount' => __( 'Sizes: %s', 'tiny-compress-images' ), + /* translators: %1$s: compressed size, e.g. 1.4 MB, %2$s: percentage saved, e.g. 56% */ + 'L10nSmaller' => __( '%1$s, %2$s smaller', 'tiny-compress-images' ), + /* translators: %s: number of images still to compress */ + 'L10nToGo' => __( '%s to go', 'tiny-compress-images' ), + /* translators: %s: number of images still to compress */ + 'L10nToCompress' => __( '%s to compress', 'tiny-compress-images' ), + 'L10nLessThanMinute' => __( + 'Less than a minute left.', 'tiny-compress-images' ), - 'L10nInitialSize' => __( 'Initial size', 'tiny-compress-images' ), - 'L10nCurrentSize' => __( 'Current size', 'tiny-compress-images' ), - 'L10nSavings' => __( 'Savings', 'tiny-compress-images' ), - 'L10nStatus' => __( 'Status', 'tiny-compress-images' ), - 'L10nShowMoreDetails' => __( - 'Show more details', + /* translators: %s: number of minutes, with the unit abbreviated so it needs no plural */ + 'L10nMinutesLeft' => __( 'About %s min left.', 'tiny-compress-images' ), + /* translators: %s: number of hours, with the unit abbreviated so it needs no plural */ + 'L10nHoursLeft' => __( 'About %s h left.', 'tiny-compress-images' ), + /* translators: %1$s: bytes saved, e.g. 14.9 MB, %2$s: number of credits used */ + 'L10nRunStatus' => __( + '%1$s saved so far. Credits used: %2$s.', 'tiny-compress-images' ), - 'L10nError' => __( 'Error', 'tiny-compress-images' ), - 'L10nLatestError' => __( 'Latest error', 'tiny-compress-images' ), + /* + * For Intl.NumberFormat, which only takes the language and region: + * WordPress variants like pt_PT_ao90 make it throw. + */ + 'locale' => implode( + '-', + array_slice( explode( '_', get_locale() ), 0, 2 ) + ), + 'L10nUnits' => array( + _x( 'B', 'unit symbol' ), + _x( 'KB', 'unit symbol' ), + _x( 'MB', 'unit symbol' ), + _x( 'GB', 'unit symbol' ), + _x( 'TB', 'unit symbol' ), + ), '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' - ), 'L10nInPageNotice' => __( // phpcs:ignore Generic.Files.LineLength 'For the plugin to do the work, you need to keep this page open. But no worries: when stopped, you can continue where you left off!', @@ -371,13 +380,6 @@ public function enqueue_scripts( $hook ) { self::version() ); - wp_enqueue_style( - self::NAME . '_chart', - plugins_url( '/css/optimization-chart.css', __FILE__ ), - array(), - self::version() - ); - wp_register_script( self::NAME . '_tiny_bulk_optimization', plugins_url( '/js/bulk-optimization.js', __FILE__ ), @@ -632,31 +634,12 @@ private function bulk_image_result( $tiny_image, array $result ) { $this->settings->get_active_tinify_sizes() ); - $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']; - - $result['initial_total_size'] = size_format( - $image_statistics['initial_total_size'], - 1 - ); - $result['optimized_total_size'] = size_format( $image_statistics['compressed_total_size'], 1 ); - $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( - 'class' => 'pinkynail', - 'alt' => '', - ) - ); + $result['savings'] = $tiny_image->get_savings( $image_statistics ); return $result; } @@ -729,10 +712,7 @@ public function ajax_bulk_queue_optimize_next() { } /** - * 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. + * Show a running background optimize status if any is running */ public function ajax_bulk_queue_status() { if ( ! check_ajax_referer( 'tiny-compress', '_nonce', false ) || @@ -741,12 +721,9 @@ public function ajax_bulk_queue_status() { wp_die( -1, 403 ); } - $ids = isset( $_POST['ids'] ) ? + $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(); @@ -765,11 +742,6 @@ public function ajax_bulk_queue_status() { $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; @@ -777,8 +749,9 @@ public function ajax_bulk_queue_status() { wp_send_json_success( array( - 'running' => $running, - 'items' => $items, + 'running' => $running, + 'items' => $items, + 'compression_count' => intval( $this->settings->get_compression_count() ), ) ); } @@ -913,27 +886,18 @@ private function render_compress_details( $tiny_image ) { } } - public function get_estimated_bulk_cost( $estimated_credit_use ) { - return Tiny_Compress::estimate_cost( - $estimated_credit_use, - $this->settings->get_compression_count() - ); - } - public function render_bulk_optimization_page() { $stats = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - - $estimated_costs = $this->get_estimated_bulk_cost( $stats['estimated_credit_use'] ); - $admin_colors = self::retrieve_admin_colors(); - - /* This makes sure that up to date information is retrieved from the API. */ $this->settings->get_compressor()->get_status(); - $active_tinify_sizes = $this->settings->get_active_tinify_sizes(); - $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(); + $widget = $this->dashboard->get_widget_data( $stats ); + $state = $widget['state']; + $compression_count = intval( $this->settings->get_compression_count() ); + + /* Without image sizes to compress, nothing is left and the widget data says done. */ + if ( 'done' === $state && 0 === count( $this->settings->get_active_tinify_sizes() ) ) { + $state = 'no_sizes'; + } /* The dashboard widget links here with a nonce to start right away. */ $start_on_load = isset( $_GET['start'], $_GET['_wpnonce'] ) && wp_verify_nonce( @@ -944,39 +908,6 @@ public function render_bulk_optimization_page() { include __DIR__ . '/views/bulk-optimization.php'; } - private static function retrieve_admin_colors() { - global $_wp_admin_css_colors; - $admin_colour_scheme = get_user_option( 'admin_color', get_current_user_id() ); - $admin_colors = array( '#0074aa', '#1685b5', '#78ca44', '#0086ba' ); // default - if ( isset( $_wp_admin_css_colors[ $admin_colour_scheme ] ) ) { - if ( isset( $_wp_admin_css_colors[ $admin_colour_scheme ]->colors ) ) { - $admin_colors = $_wp_admin_css_colors[ $admin_colour_scheme ]->colors; - } - } - if ( '#e5e5e5' == $admin_colors[0] && '#999' == $admin_colors[1] ) { - $admin_colors[0] = '#bbb'; - } - if ( '#5589aa' == $admin_colors[0] && '#cfdfe9' == $admin_colors[1] ) { - $admin_colors[1] = '#85aec5'; - } - if ( '#7c7976' == $admin_colors[0] && '#c6c6c6' == $admin_colors[1] ) { - $admin_colors[1] = '#adaba9'; - $admin_colors[2] = '#adaba9'; - } - if ( self::wp_version() > 3.7 ) { - if ( 'fresh' == $admin_colour_scheme ) { - $admin_colors = array( '#0074aa', '#1685b5', '#78ca44', '#0086ba' ); // better - } - } - return $admin_colors; - } - - public function friendly_user_name() { - $user = wp_get_current_user(); - $name = ucfirst( empty( $user->first_name ) ? $user->display_name : $user->first_name ); - return $name; - } - /** * Will clean up converted files (if any) when the original is deleted * @@ -1020,19 +951,6 @@ public function backup_original_image( $attachment_id, $wp_metadata = null ) { return $tiny_image->create_backup(); } - public static function request_review() { - $review_url = - 'https://wordpress.org/support/plugin/tiny-compress-images/reviews/#new-post'; - $review_block = esc_html__( 'Enjoying TinyPNG?', 'tiny-compress-images' ); - $review_block .= ' '; - $review_block .= sprintf( - '%s', - esc_url( $review_url ), - esc_html__( 'Write a review', 'tiny-compress-images' ) - ); - return $review_block; - } - /** * Runs on uninstall * diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index 6ebfc15a..e31ff1ce 100644 --- a/src/class-tiny-settings.php +++ b/src/class-tiny-settings.php @@ -193,9 +193,7 @@ public function image_sizes_notice() { } public function account_status() { - if ( current_user_can( 'manage_options' ) ) { - $this->render_account_status(); - } + $this->render_account_status(); exit(); } @@ -706,6 +704,22 @@ public function render_size_checkboxes_description( } } + /** + * Renders the card that explains how credits are used, for pages + * other than the settings page. + * + * @param bool $show_settings_link Whether the card links to the settings page. + */ + public function render_credits( $show_settings_link ) { + $tiny_sizes_count = count( + array_diff( $this->get_active_tinify_sizes(), array( Tiny_Image::ORIGINAL_UNSCALED ) ) + ); + $tiny_resize = $this->get_resize_enabled(); + $tiny_wr2x = $this->compress_wr2x_images(); + $tiny_convert = $this->get_conversion_enabled(); + include __DIR__ . '/views/settings-credits.php'; + } + public function render_credits_per_image( $credits ) { echo '

'; printf( @@ -800,9 +814,14 @@ public function after_compress_callback( $compressor ) { } } - public function render_account_status() { + /** + * @param bool $link_to_settings Whether Change account links to the settings + * page, for pages other than the settings page. + */ + public function render_account_status( $link_to_settings = false ) { + $is_allowed = current_user_can( 'manage_options' ); $key = $this->get_api_key(); - if ( ! empty( $key ) ) { + if ( $is_allowed && ! empty( $key ) ) { if ( $this->refresh_api_key_pending() ) { $status = (object) array( 'ok' => true, diff --git a/src/css/bulk-optimization.css b/src/css/bulk-optimization.css index 2b22e15b..6602ab93 100644 --- a/src/css/bulk-optimization.css +++ b/src/css/bulk-optimization.css @@ -1,356 +1,267 @@ -div.tiny-bulk-optimization div.dashboard { - min-width: 750px; - margin-top: 10px; - border: 1px solid #e5e5e5; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); - background-color: #fff; -} - -@media only screen and (max-width: 799px) { - div.tiny-bulk-optimization div.dashboard { - min-width: 250px; - } -} - -div.tiny-bulk-optimization div.available { - float: left; - width: 50%; -} - -@media only screen and (max-width: 799px) { - div.tiny-bulk-optimization div.available { - width: 100%; - } -} - -div.tiny-bulk-optimization div.available div.inner { - max-width: 600px; - padding: 4px 23px 23px; -} - -div.tiny-bulk-optimization div.available table.totals { - max-width: 500px; - width: 100%; - margin: 18px 0; - border: 1px solid #e5e5e5; - border-spacing: 1px; - box-shadow: inset 0 0 0 25px #fff; - background-color: #e5e5e5; -} - -/*ie6-8*/ div.tiny-bulk-optimization div.available table.totals { - border-collapse: collapse \9; - border-spacing: 0 \9; -} - -/*ie6-8*/ div.tiny-bulk-optimization div.available table.totals td { - border: 1px solid #e5e5e5 \9; -} - -div.tiny-bulk-optimization div.available table.totals td.item { - width: 33%; - padding: 1em 8px; - vertical-align: top; +.tiny-bulk-hero { + align-items: center; + gap: 0; + padding: 40px 24px; text-align: center; - background-color: #fff; } -@media only screen and (max-width: 400px) { - div.tiny-bulk-optimization div.available table.totals td.item.costs { - display: none; - } -} - -div.tiny-bulk-optimization div.available table.totals td.item h3 { - margin-top: 0; - text-transform: uppercase; - color: #bbb; - font-size: 1.1em; - font-weight: normal; -} - -div.tiny-bulk-optimization div.available table.totals td.item span { - font-size: 1.8em; - font-weight: bold; - line-height: 1em; -} - -div.tiny-bulk-optimization div.available table.totals td.item div.tooltip { - position: relative; - display: inline-block; - vertical-align: super; -} - -div.tiny-bulk-optimization div.available table.totals td.item div.tooltip span.dashicons { - font-weight: normal; - font-size: 1.3em; - /* WordPress color embedded in webpage */ -} - -* + html div.tiny-bulk-optimization div.available table.totals td.item div.tooltip span.dashicons { +.tiny-bulk-hero[hidden] { display: none; } -div.tiny-bulk-optimization div.available table.totals td.item div.tooltip div.tip { - position: absolute; - z-index: 10; - display: none; - top: 50%; - left: 130%; - width: 18em; - padding: 0 1em; - background-color: #fff; - box-shadow: 1px 1px 4px rgba(0,0,0,0.2); - border: 1px solid #e5e5e5; - text-align: left; +.tiny-bulk-panda { + width: 128px; + height: 128px; + margin-bottom: 16px; } -div.tiny-bulk-optimization div.available div.notes { - display: table; +.tiny-bulk-badge { + padding: 2px 10px; + border: 1px solid currentcolor; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + line-height: 20px; } -div.tiny-bulk-optimization div.available div.notes h4 { - display: table-cell; - font-size: 1.1em; - padding-right: 1.2em; - white-space: nowrap; +.tiny-bulk-badge-red { + background: #fef2f2; + color: #dc2626; } -div.tiny-bulk-optimization div.available div.notes p { - display: table-cell; +.tiny-bulk-badge-amber { + background: #fef3c7; + color: #b45309; } -div.tiny-bulk-optimization div.savings { - margin-left: 50%; +.tiny-bulk-badge-green { + background: #e8faf0; + color: #00b075; } -@media only screen and (max-width: 799px) { - div.tiny-bulk-optimization div.savings { - margin-left: 0; - } +.tiny-bulk-optimization .tiny-bulk-title { + margin: 12px 0 0; + padding: 0; + color: var(--tiny-text-primary); + font-size: 30px; + font-weight: 600; + line-height: 38px; } -div.tiny-bulk-optimization div.savings div.inner { - max-width: 600px; - padding: 4px 23px 23px; +.tiny-bulk-hero p { + max-width: 540px; + margin: 12px 0 0; + color: var(--tiny-text-secondary); + font-size: 16px; + line-height: 24px; } -div.tiny-bulk-optimization div.savings div.legend { - margin-left: 50%; -} - -div.tiny-bulk-optimization div.savings div.legend { - /* margin-left defined inline */ - padding: 22px 0; +.tiny-bulk-hero .tiny-bulk-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 16px; } -div.tiny-bulk-optimization div.savings table { - margin: 0 auto; +.tiny-bulk-hero .tiny-bulk-caption { + font-size: 14px; + line-height: 20px; } -div.tiny-bulk-optimization div.savings table td.value { - font-size: 1.4em; - vertical-align: top; - text-align: right; - padding: 15px 8px 0 5px; - white-space: nowrap; +.tiny-bulk-hero .tiny-bulk-caption:empty { + display: none; } -div.tiny-bulk-optimization div.savings table td.description { - padding: 19px 0 0; - line-height: 14px; - vertical-align: top; - text-align: left; - color: #bbb; +.tiny-bulk-progress { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + max-width: 360px; + margin-top: 16px; + color: var(--tiny-text-primary); + font-size: 14px; + font-weight: 600; } -@media only screen and (max-width: 400px) { - div.tiny-bulk-optimization div.savings div.legend { - margin-left: 0 !important; - padding: 10px 0; - } +.tiny-bulk-progress .tiny-bulk-bar { + flex: 1; } -div.tiny-bulk-optimization div.optimize { - margin: 4px 23px 32px; - text-align: center; - clear: both; +.tiny-bulk-bar { + height: 6px; + overflow: hidden; + background: var(--tiny-surface-subtle); + border-radius: 3px; } -div.tiny-bulk-optimization div.optimize div.progressbar { - position: relative; - height: 32px; - margin-bottom: 32px; - line-height: 32px; - border-radius: 32px; - font-size: 1.2em; - font-weight: bold; - color: #fff; - background-color: #ebebeb; -} - -div.tiny-bulk-optimization div.optimize div.progressbar div.progress { - position: absolute; - left: 0; - min-width: 32px; - width: 0; - height: 32px; - z-index: 2; - border-radius: 16px; - transition: width 1s ease; - background-size: 40px 80px; - -webkit-backface-visibility: hidden; - -webkit-perspective: 1000; - backface-visibility: hidden; - perspective: 1000; +.tiny-bulk-bar span { + display: block; + height: 100%; + background: var(--tiny-green-500); + transition: width 0.3s; } -@keyframes progress-bar { - to { - background-position: 3200px 0; - } +.tiny-bulk-library { + gap: 12px; } -div.tiny-bulk-optimization div.optimize div.progressbar div.numbers { - position: relative; - z-index: 3; - text-shadow: 1px 1px 3px rgba(0,0,0,0.2); +.tiny-bulk-library-head, +.tiny-bulk-legend { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 8px; } -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero { - display: none; +.tiny-bulk-optimization h3 { + margin: 0; + color: var(--tiny-text-primary); + font-size: 16px; + font-weight: 600; + line-height: 24px; } -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero.visible { - display: inline; +.tiny-bulk-library-head span, +.tiny-bulk-legend-todo { + color: var(--tiny-text-secondary); } -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero { - min-width: 230px; +.tiny-bulk-legend { + color: var(--tiny-text-primary); + font-size: 14px; } -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero.red { - /* WP < 3.8 */ background-image: none; - border-color: #920011; - background-color: #d0021b; - box-shadow: 0 2px 0 #920011 !important; - text-shadow: 0 -1px 1px #920011, 1px 0 1px #920011, 0 1px 1px #920011, -1px 0 1px #920011; +.tiny-bulk-legend span:before { + content: ""; + display: inline-block; + width: 10px; + height: 10px; + margin-right: 8px; + border-radius: 2px; } -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero.red:focus, -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero.red:hover { - background-color: #e00016; +.tiny-bulk-legend-done:before { + background: var(--tiny-green-500); } -div.tiny-bulk-optimization div.optimize div.optimization-buttons input.button-hero.red:active { - background-color: #d0021b; - box-shadow: 0 3px 0 #920011 inset !important; +.tiny-bulk-legend-todo:before { + background: var(--tiny-border-emphasis); } -div.tiny-bulk-optimization div.optimize div.optimization-buttons p.optimization-buttons_notice { - max-width: 420px; - margin: 10px auto 0 auto; -} -div.tiny-bulk-optimization div.optimize div#optimization-spinner { - position: absolute; +.tiny-bulk-legend span span:before { display: none; - width: 20px; - height: 20px; - margin-top: 15px; - background-size: 20px 20px; -} - -div.tiny-bulk-optimization table.whitebox { - min-width: 750px; - margin-top: 16px; - border: 1px solid #e5e5e5; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); - background-color: #fff; } -@media only screen and (max-width: 799px) { - div.tiny-bulk-optimization table.whitebox { - min-width: 250px; - } +.tiny-bulk-list { + gap: 8px; } -div.tiny-bulk-optimization table.whitebox th.column-primary { - width: 30%; +.tiny-bulk-items { + margin: 0; } -@media only screen and (max-width: 782px) { - div.tiny-bulk-optimization table.whitebox th.column-primary { - width: 100%; - } +.tiny-bulk-item { + display: flex; + align-items: center; + gap: 12px; + margin: 0; + padding: 8px 0; + border-top: 1px solid var(--tiny-border-default); } -div.tiny-bulk-optimization table.whitebox th.column-author { - /* WP 3.1-4.2 */ width: auto; +.tiny-bulk-item:first-child { + border-top: 0; } -div.tiny-bulk-optimization table.whitebox thead th.thumbnail { - width: 30px; -} - -div.tiny-bulk-optimization table.whitebox th.thumbnail { - width: 30px; +.tiny-bulk-item img { + flex: none; + width: 32px; height: 32px; - padding-top: 6px; - vertical-align: top; -} - -div.tiny-bulk-optimization table.whitebox td { - padding-top: 12px; + object-fit: cover; + background: var(--tiny-surface-subtle); + border-radius: 4px; } -div.tiny-bulk-optimization table.whitebox td.name, -div.tiny-bulk-optimization table.whitebox td.status { - padding-bottom: 12px; +.tiny-bulk-item-name { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; } -div.tiny-bulk-optimization table.whitebox td.column-primary button { - margin-top: 6px; +.tiny-bulk-item-name strong, +.tiny-bulk-item-name span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -div.tiny-bulk-optimization button.toggle-row { - /* WP < 4.3 */ display: none; +.tiny-bulk-item-name strong { + color: var(--tiny-text-primary); + font-size: 14px; + font-weight: 600; } -div.tiny-bulk-optimization table.whitebox tr.success button.toggle-row:before { - color: #7acb44; - content: "\f147"; /* yes */ +.tiny-bulk-item-name span { + color: var(--tiny-text-secondary); + font-size: 12px; } -div.tiny-bulk-optimization table.whitebox tr.failed button.toggle-row:before { - color: #d54e21; - content: "\f158"; /* more compatibility*/ +.tiny-bulk-item-status { + flex: none; + max-width: 50%; + overflow: hidden; + color: var(--tiny-text-secondary); + font-size: 14px; + text-overflow: ellipsis; + white-space: nowrap; } -div.tiny-bulk-optimization table.whitebox tr.no-action button.toggle-row:before { - color: gray; - content: "\f158"; /* more compatibility*/ +.tiny-bulk-item[data-status="compressing"] .tiny-bulk-item-status, +.tiny-bulk-item[data-status="compressed"] .tiny-bulk-item-status { + color: var(--tiny-text-primary); } -div.tiny-bulk-optimization table.whitebox tr.failed.is-expanded span.icon { - display: none; +.tiny-bulk-item[data-status="compressing"] .tiny-bulk-item-status:before { + content: ""; + display: inline-block; + width: 12px; + height: 12px; + margin-right: 8px; + vertical-align: -2px; + border: 2px solid var(--tiny-surface-subtle); + border-top-color: var(--tiny-green-500); + border-radius: 50%; + animation: tiny-bulk-spin 1s linear infinite; } -div.tiny-bulk-optimization div.upgrade-account-notice { - padding: 22px 28px; - background-color: #F5F9FA; - text-align: center; +.tiny-bulk-item[data-status="error"] .tiny-bulk-item-status { + color: var(--tiny-red-500); } -div.tiny-bulk-optimization div.upgrade-account-notice div.introduction { - padding-bottom: 10px; +@keyframes tiny-bulk-spin { + to { + transform: rotate(360deg); + } } -div.tiny-bulk-optimization div.upgrade-account-notice p { - margin: 5px 0; +@media (prefers-reduced-motion: reduce) { + .tiny-bulk-item[data-status="compressing"] .tiny-bulk-item-status:before { + animation: none; + } } -div.tiny-bulk-optimization div.upgrade-account-notice p a { - display: block; - margin-top: 10px; - color: #858A93; +.tiny-bulk-footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 8px; + padding: 16px 24px; + color: var(--tiny-text-secondary); + font-size: 14px; + background: var(--tiny-surface-page); + border-top: 1px solid var(--tiny-border-default); + border-radius: 0 0 12px 12px; } diff --git a/src/css/optimization-chart.css b/src/css/optimization-chart.css deleted file mode 100644 index 8d3b6f4a..00000000 --- a/src/css/optimization-chart.css +++ /dev/null @@ -1,52 +0,0 @@ -div.tiny-optimization-chart { - float: left; - position: relative; - width: 50%; - text-align: center; -} - -.ie8 div.tiny-optimization-chart { - margin: 0; -} - -div.tiny-optimization-chart svg circle.main { - fill: #ebebeb; - animation: shwoosh 2s ease; - transition: stroke-dasharray 1s ease; -} - -div.tiny-optimization-chart svg circle.inner { - fill: #fff; -} - -div.tiny-optimization-chart div.value { - position: absolute; - top: 0; - left: 0; - width: 100%; - text-align: center; -} - -div.tiny-optimization-chart div.value div.percentage { - width: 50%; - margin-left: auto; - margin-right: auto; - margin-top: 56px; - font-size: 2.1em; - line-height: 1em; -} - -div.tiny-optimization-chart div.value div.label { - width: 50%; - margin-left: auto; - margin-right: auto; - font-size: 1.4em; - line-height: 1.2em; -} - -@media only screen and (max-width: 400px) { - div.tiny-bulk-optimization div.tiny-optimization-chart { - float: none; - width: 100%; - } -} diff --git a/src/js/bulk-optimization.js b/src/js/bulk-optimization.js index 5da2a34e..8cec388e 100644 --- a/src/js/bulk-optimization.js +++ b/src/js/bulk-optimization.js @@ -1,352 +1,333 @@ (function() { const POLL_INTERVAL = 2000; const POLL_WINDOW = 10; + const POLL_ATTEMPTS = 5; const PAGE_WORKERS = 5; + const config = window.tinyBulk; + const items = config.items; let pending = []; - let stoppedEarly = false; let pageWorkers = 0; + let finished = 0; + let compressed = 0; + let savedBytes = 0; + let creditsUsed = 0; + let failedPolls = 0; + + function findPageElement(name) { + return document.querySelector(`[data-tiny-bulk="${name}"]`); + } - function updateProgressBar(successFullCompressions) { - var totalToOptimize = parseInt(jQuery('div#compression-progress-bar').data('number-to-optimize'), 10); - - var optimizedSoFar = parseInt(jQuery('#optimized-so-far').text(), 10); - jQuery('#optimized-so-far').html(successFullCompressions + optimizedSoFar); - - var percentage = '100%'; - if (totalToOptimize > 0) { - percentage = Math.round((successFullCompressions + optimizedSoFar) / totalToOptimize * 100, 1) + '%'; + function setText(name, text) { + const target = findPageElement(name); + if (target) { + target.textContent = text; } - jQuery('div#compression-progress-bar #progress-size').css('width', percentage); - jQuery('div#compression-progress-bar #percentage').html('(' + percentage + ')'); - - var numberToOptimize = parseInt(jQuery('#optimizable-image-sizes').html(), 10); - jQuery('#optimizable-image-sizes').html(numberToOptimize - successFullCompressions); } - function updateSavings(successFullCompressions, successFullSaved, newHumanReadableLibrarySize) { - window.currentLibraryBytes = window.currentLibraryBytes + successFullSaved; + // Fills in %s, or %1$s, %2$s, like PHP's sprintf. + function format(template) { + const values = Array.prototype.slice.call(arguments, 1); + let next = 0; + return template.replace(/%(?:(\d)\$)?s/g, function(match, position) { + return position ? values[position - 1] : values[next++]; + }); + } - var imagesSizedOptimized = parseInt(jQuery('#optimized-image-sizes').text(), 10) + successFullCompressions; - var initialLibraryBytes = parseInt(jQuery('#unoptimized-library-size').data('bytes'), 10); - var percentage = (1 - window.currentLibraryBytes / initialLibraryBytes); - var chartSize = jQuery('div#optimization-chart').data('full-circle-size'); + // Numbers follow the WordPress locale, like PHP's number_format_i18n. + const numberFormat = new Intl.NumberFormat(tinyCompress.locale); + const sizeFormat = new Intl.NumberFormat(tinyCompress.locale, { + minimumFractionDigits: 1, + maximumFractionDigits: 1 + }); - jQuery('#optimized-image-sizes').html(imagesSizedOptimized); - jQuery('#optimized-library-size').attr('data-bytes', window.currentLibraryBytes); - jQuery('#optimized-library-size').html(newHumanReadableLibrarySize); - jQuery('#savings-percentage').html(Math.round(percentage * 1000) / 10 + '%'); - jQuery('div#optimization-chart svg circle.main').css('stroke-dasharray', '' + (chartSize * percentage) + ' ' + chartSize); + /** + * Formats bytes in the largest unit that keeps the value at 1 or more, with + * one decimal like PHP's size_format( $bytes, 1 ). Nothing or less than + * nothing shows as 0 bytes. + * @param number bytes + */ + function formatBytes(bytes) { + const units = tinyCompress.L10nUnits; + let value = Math.max(0, bytes || 0); + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit++; + } + const valueFormat = unit === 0 ? numberFormat : sizeFormat; + return `${valueFormat.format(value)} ${units[unit]}`; } - function handleCancellation() { - jQuery('div#bulk-optimization-actions').hide(); - jQuery('div.progress').css('animation', 'none'); + async function post(action, data) { + const body = new URLSearchParams(Object.assign({ _nonce: tinyCompress.nonce, action: action }, data)); + const response = await fetch(ajaxurl, { method: 'POST', body: body, credentials: 'same-origin' }); + if (!response.ok) { + const error = new Error(response.statusText); + error.status = response.status; + throw error; + } + const json = await response.json(); + return json.data; } - function updateRowAfterCompression(row, data) { - var successFullCompressions = parseInt(data.success, 10); - var successFullSaved = parseInt(data.size_change, 10); - var newHumanReadableLibrarySize = data.human_readable_library_size; - if (successFullCompressions === 0) { - row.addClass('no-action'); - row.find('.status').html('' + tinyCompress.L10nNoActionTaken).attr('data-status', 'no-action-taken'); - } else { - row.addClass('success'); - const rowResultElement = row.find('.status'); - rowResultElement.attr('data-status', 'compressed'); - const icon = ''; - - let successHTML = ''; - if (data.image_sizes_compressed > 0) { - successHTML += `

${icon} ${data.image_sizes_compressed} ${tinyCompress.L10nCompressed}

`; - } + function showHero(state) { + document.querySelectorAll('[data-tiny-bulk-hero]').forEach(function(hero) { + hero.hidden = hero.getAttribute('data-tiny-bulk-hero') !== state; + }); + } - if (data.image_sizes_converted > 0) { - successHTML += `

${icon} ${data.image_sizes_converted} ${tinyCompress.L10nConverted}

`; + function drawRows() { + const list = document.getElementById('tiny-bulk-items'); + if (!list) { + return; + } + items.forEach(function(item, i) { + const row = document.createElement('li'); + row.className = 'tiny-bulk-item'; + row.setAttribute('data-status', 'waiting'); + + const thumbnail = document.createElement('img'); + thumbnail.loading = 'lazy'; + thumbnail.alt = ''; + if (item.thumbnail) { + thumbnail.src = item.thumbnail; } - rowResultElement.html(successHTML); - - updateProgressBar(successFullCompressions); - updateSavings(successFullCompressions, successFullSaved, newHumanReadableLibrarySize); - } + const name = document.createElement('div'); + name.className = 'tiny-bulk-item-name'; + const title = document.createElement('strong'); + title.textContent = item.post_title; + const details = document.createElement('span'); + const sizeCount = format(tinyCompress.L10nSizeCount, numberFormat.format(item.sizes)); + details.textContent = `${item.file}, ${sizeCount}, ${formatBytes(item.bytes)}`; + name.append(title, details); + + const status = document.createElement('span'); + status.className = 'tiny-bulk-item-status'; + status.setAttribute('data-testid', `bulk-item-status-${i}`); + status.textContent = tinyCompress.L10nWaiting; + + row.append(thumbnail, name, status); + list.append(row); + }); } - function bulkOptimizationCallback(data, items, i) { - var row = jQuery('#optimization-items tr').eq(parseInt(i, 10)+1); + function setRowStatus(i, status, text) { + const row = document.getElementById('tiny-bulk-items').children[i]; + row.setAttribute('data-status', status); + row.querySelector('.tiny-bulk-item-status').textContent = text; + return row; + } - if (data.failed > 0) { - row.addClass('failed'); - row.find('.status').html('' + tinyCompress.L10nLatestError + ': ' + data.message + ''); - row.find('.status').attr('title', data.message); - row.find('.status').attr('data-status', 'error'); + function applyResult(i, result) { + finished++; + if (result.failed > 0) { + setRowStatus(i, 'error', result.message).title = result.message; + } else if (parseInt(result.success, 10) === 0) { + setRowStatus(i, 'no-action', tinyCompress.L10nNoActionTaken); } else { - updateRowAfterCompression(row, data); + compressed++; + savedBytes -= parseInt(result.size_change, 10); + setRowStatus(i, 'compressed', format( + tinyCompress.L10nSmaller, + result.optimized_total_size, + `${Math.round(result.savings)}%` + )); } + } - row.find('.name').html(items[i].post_title + ''); - - if (!data.image_sizes_compressed) { - data.image_sizes_compressed = '-'; - } - if (!data.initial_total_size) { - data.initial_total_size = '-'; - } - if (!data.optimized_total_size) { - data.optimized_total_size = '-'; + function secondsLeftText() { + const sizesLeft = pending.reduce(function(sum, i) { + return sum + items[i].sizes; + }, 0); + const seconds = Math.round(sizesLeft / config.workers); + if (seconds < 60) { + return tinyCompress.L10nLessThanMinute; } - if (!data.savings || data.savings === 0) { - data.savings = '-'; - } else { - data.savings += '%'; + if (seconds < 90 * 60) { + return format(tinyCompress.L10nMinutesLeft, numberFormat.format(Math.round(seconds / 60))); } + return format(tinyCompress.L10nHoursLeft, numberFormat.format(Math.round(seconds / 3600))); + } - row.find('.thumbnail').html(data.thumbnail); - row.find('.sizes-compressed').html(data.image_sizes_compressed); - row.find('.initial-size').html(data.initial_total_size); - row.find('.optimized-size').html(data.optimized_total_size); - row.find('.savings').html(data.savings); + function updateNumbers() { + const hasItems = items.length > 0; + const itemsRunningPerc = hasItems ? Math.floor(finished / items.length * 100) : 0; + const runPercentage = `${itemsRunningPerc}%`; + setText('run-done', numberFormat.format(finished)); + setText('run-percentage', runPercentage); + findPageElement('run-bar').style.width = runPercentage; + const runStatus = format( + tinyCompress.L10nRunStatus, + formatBytes(savedBytes), + numberFormat.format(creditsUsed) + ); + setText('run-status', `${secondsLeftText()} ${runStatus}`); + + const libraryCompressed = config.imagesOptimized + compressed; + const libraryPercentage = Math.floor(libraryCompressed / config.imagesTotal * 100); + setText('library-percentage', numberFormat.format(libraryPercentage)); + setText('library-compressed', numberFormat.format(libraryCompressed)); + setText('library-todo', format(tinyCompress.L10nToGo, numberFormat.format(config.imagesRemaining - compressed))); + findPageElement('library-bar').style.width = `${libraryPercentage}%`; + setText('waiting', numberFormat.format(pending.length)); + + const librarySaved = config.bytesSaved + savedBytes; + setText('stat-compressed', numberFormat.format(libraryCompressed)); + setText('stat-saved', formatBytes(librarySaved)); + setText('stat-average', `${Math.round(librarySaved / config.initialSize * 100)}%`); + setText('done-saved', formatBytes(librarySaved)); } - function finishOptimization(message) { - if (message) { - const notice = jQuery('

'); - notice.find('p').text(message); - notice.insertAfter(jQuery('#tiny-bulk-optimization h2')); + // Shows the done hero when every image was compressed. Otherwise the run was + // paused or stopped, some images failed, or the page had no images listed, + // and the page reloads to show what is left. + function finishRun() { + if (items.length === 0 || pending.length > 0 || compressed < items.length) { + location.reload(); + return; } - jQuery('div#optimization-spinner').css('display', 'none'); - handleCancellation(); + const title = findPageElement('list-title'); + title.textContent = title.getAttribute('data-done'); + findPageElement('list-waiting').hidden = true; + setText('library-todo', format(tinyCompress.L10nToCompress, '0')); + showHero('done'); } // 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) { + async function pollStatus() { 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; + try { + const data = await post('tiny_bulk_queue_status', { + ids: batch.map(function(i) { return items[i].ID; }).join(',') + }); const running = data.running || pageWorkers > 0; - let finished = 0; - const waiting = batch.filter(function(i) { + const finishedBefore = finished; + creditsUsed = data.compression_count - config.compressionCount; + + batch.forEach(function(i) { const item = data.items[items[i].ID] || {}; if (item.status === 'done' || item.status === 'failed') { - bulkOptimizationCallback(item.result, items, i); - finished++; - } else if (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; + applyResult(i, item.result); + pending = pending.filter(function(p) { return p !== i; }); + } else if (item.status === 'processing') { + setRowStatus(i, 'compressing', tinyCompress.L10nCompressing); } - return false; }); - pending = waiting.concat(pending.slice(batch.length)); + updateNumbers(); + failedPolls = 0; - if (pending.length > 0 && (running || finished > 0)) { - setTimeout(pollStatus, POLL_INTERVAL, items); - } else if (window.optimizationCancelled) { - finishOptimization(null); + // A run can still be going when this page has no images waiting for it, + // e.g. when the page loaded just as the run was finishing up. + if (running || (pending.length > 0 && finished > finishedBefore)) { + setTimeout(pollStatus, POLL_INTERVAL); } else { - finishOptimization(stoppedEarly ? tinyCompress.L10nStoppedNotice : tinyCompress.L10nAllDone); + finishRun(); } - }, 'json').fail(function(xhr) { - if (xhr.status === 403) { - finishOptimization(tinyCompress.L10nInternalError); + } catch (error) { + failedPolls++; + if (failedPolls < POLL_ATTEMPTS) { + setTimeout(pollStatus, POLL_INTERVAL); } else { - setTimeout(pollStatus, POLL_INTERVAL, items); + setText('run-notice', tinyCompress.L10nInternalError); } - }); + } } /** * When the page cannot reach itself (loopbacks are not supported), * the page works through the queue one image per request until none are left */ - function optimizeInPage() { - jQuery.post(ajaxurl, { - _nonce: tinyCompress.nonce, - action: 'tiny_bulk_queue_optimize_next' - }, function(response) { - if (response.data.optimized) { + async function optimizeInPage() { + try { + const data = await post('tiny_bulk_queue_optimize_next'); + if (data.optimized) { optimizeInPage(); } else { pageWorkers--; } - }, 'json').fail(function(xhr) { - if (xhr.status === 403) { + } catch (error) { + if (error.status === 403) { pageWorkers--; } else { setTimeout(optimizeInPage, POLL_INTERVAL); } - }); - } - - function prepareBulkOptimization(items, running, startOnLoad) { - window.allBulkOptimizationItems = items; - updateProgressBar(0); - if (running) { - startBulkOptimization(items, true); } - startFromUrl(items, running, startOnLoad); } - // The dashboard widget opens this page with &start=1 and a nonce, which the - // server checks. Start right away, then drop both from the URL so a reload - // does not start again. - function startFromUrl(items, running, startOnLoad) { - const url = new URL(location.href); - if (url.searchParams.has('start')) { - url.searchParams.delete('start'); - url.searchParams.delete('_wpnonce'); - history.replaceState(null, '', url); - } - if (!startOnLoad || running) { - return; - } - - const start = document.getElementById('id-start'); - if (start && start.offsetParent !== null) { - jQuery('#hide-warning').trigger('click'); - startBulkOptimization(items); + function showRun() { + const title = findPageElement('list-title'); + if (title) { + title.textContent = title.getAttribute('data-compressing'); } + showHero('compressing'); + updateNumbers(); } - function startBulkOptimization(items, running) { - window.optimizationCancelled = false; - window.totalRowsDrawn = 0; - window.currentLibraryBytes = parseInt(jQuery('#optimized-library-size').data('bytes'), 10); + async function startRun() { pending = items.map(function(item, i) { return i; }); - stoppedEarly = false; - - const notice = jQuery('div#bulk-optimization-actions p.optimization-buttons_notice'); - jQuery('div#bulk-optimization-actions input').removeClass('visible'); - jQuery('div.progress').css('animation', 'progress-bar 80s linear infinite'); - jQuery('div#optimization-spinner').css('display', 'inline-block'); - updateProgressBar(0); - - if (running) { - notice.text(tinyCompress.L10nBackgroundNotice); - 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(response) { - if (response.data.background) { - notice.text(tinyCompress.L10nBackgroundNotice); + showRun(); + try { + const data = await post('tiny_bulk_queue_start'); + if (data.background) { + setText('run-notice', tinyCompress.L10nBackgroundNotice); } else { - notice.text(tinyCompress.L10nInPageNotice); + setText('run-notice', tinyCompress.L10nInPageNotice); for (let i = 0; i < PAGE_WORKERS; i++) { pageWorkers++; optimizeInPage(); } } - jQuery('div#bulk-optimization-actions input#id-optimizing').addClass('visible'); - pollStatus(items); - }, 'json').fail(function() { - finishOptimization(tinyCompress.L10nInternalError); - }); + pollStatus(); + } catch (error) { + setText('run-notice', tinyCompress.L10nInternalError); + } } - function drawSomeRows(items, end) { - const list = jQuery('#optimization-items tbody'); - const start = window.totalRowsDrawn; - end = Math.min(end, items.length); - for (let i = start; i < end; i++) { - const tableRow = ` - - ${items[i].post_title} - - - - ${tinyCompress.L10nWaiting} - `; - list.append(tableRow); + async function pauseRun() { + const button = findPageElement('pause'); + if ( !button ) { + return; } - window.totalRowsDrawn = Math.max(start, end); - } - async function cancelOptimization() { + button.disabled = true; + button.textContent = tinyCompress.L10nPausing; 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); + await post('tiny_bulk_queue_cancel'); + } catch (error) { + button.disabled = false; + button.textContent = tinyCompress.L10nPause; + setText('run-notice', tinyCompress.L10nInternalError); } } - jQuery('.tiny-bulk-optimization .upgrade-account-notice a#hide-warning').click(function() { - jQuery('.tiny-bulk-optimization .upgrade-account-notice').hide(); - jQuery('.tiny-bulk-optimization .optimize').children().show(); - }); + // The dashboard widget opens this page with &start=1 and a nonce. + // Both are removed from the URL so a reload does not start again. + const url = new URL(location.href); + if (url.searchParams.has('start')) { + url.searchParams.delete('start'); + url.searchParams.delete('_wpnonce'); + history.replaceState(null, '', url); + } - jQuery('div#bulk-optimization-actions input').click(function() { - if ((jQuery(this).attr('id') === 'id-start') && jQuery(this).hasClass('visible')) { - startBulkOptimization(window.allBulkOptimizationItems); - } - if ((jQuery(this).attr('id') === 'id-cancel') && jQuery(this).hasClass('visible')) { - cancelOptimization(); - } - }); + drawRows(); - jQuery('div#bulk-optimization-actions input').hover(function() { - if ((jQuery(this).attr('id') === 'id-optimizing') && jQuery(this).hasClass('visible')) { - window.lastActiveButton = jQuery('div#bulk-optimization-actions input.visible'); - window.lastActiveButton.removeClass('visible'); - jQuery('div#bulk-optimization-actions input#id-cancel').addClass('visible'); - } - }, function() { - if ((jQuery(this).attr('id') === 'id-cancel') && jQuery(this).hasClass('visible')) { - window.lastActiveButton.addClass('visible'); - jQuery('div#bulk-optimization-actions input#id-cancel').removeClass('visible'); - } + document.querySelectorAll('[data-tiny-bulk="start"]').forEach(function(button) { + button.addEventListener('click', startRun); }); - function attachToolTipEventHandlers() { - var tooltip = '#tiny-bulk-optimization div.tooltip'; - var tip = 'div.tip'; - var toolTipTimeout = null; - jQuery(tooltip).mouseleave(function(){ - var that = this; - toolTipTimeout = setTimeout(function() { - if (jQuery(that).find(tip).is(':visible')) { - jQuery(tooltip).find(tip).hide(); - } - }, 100); - }); - jQuery(tooltip).mouseenter(function(){ - jQuery(this).find(tip).show(); - clearTimeout(toolTipTimeout); - }); - jQuery(tooltip).find(tip).mouseenter(function(){ - clearTimeout(toolTipTimeout); - }); + const pauseElement = findPageElement('pause'); + if (pauseElement) { + pauseElement.addEventListener('click', pauseRun); } - attachToolTipEventHandlers(); - - window.bulkOptimizationAutorun = startBulkOptimization; - window.bulkOptimization = prepareBulkOptimization; - + if (config.running) { + pending = items.map(function(item, i) { return i; }); + setText('run-notice', tinyCompress.L10nBackgroundNotice); + showRun(); + pollStatus(); + } else if (config.startOnLoad && findPageElement('start')) { + startRun(); + } }).call(); diff --git a/src/views/account-status-connected.php b/src/views/account-status-connected.php index d1f01bfe..19d20239 100644 --- a/src/views/account-status-connected.php +++ b/src/views/account-status-connected.php @@ -9,6 +9,7 @@ * @type string|null $message Optional status message. * } * @var string $key The API key. + * @var bool $link_to_settings Whether Change account links to the settings page. */ // Every account gets free credits each month. Only free accounts have a @@ -150,6 +151,10 @@ ); ?> + + + + + + + = 1 ) { ?> + + + +

+

+ +

+ + +

+ 0', + esc_html( number_format_i18n( count( $items ) ) ) + ); + ?> +

+
+
+ +
+ 0% +
+

+

+ +

+

+ + +

+ +

+

+ ' . esc_html( size_format( $widget['bytes_saved'], 0 ) ) . '' + ); + if ( $widget['auto_compress'] ) { + echo ' '; + esc_html_e( 'New uploads are compressed automatically.', 'tiny-compress-images' ); + } + ?> +

+ + diff --git a/src/views/bulk-optimization-upgrade-notice.php b/src/views/bulk-optimization-upgrade-notice.php deleted file mode 100644 index f2272b27..00000000 --- a/src/views/bulk-optimization-upgrade-notice.php +++ /dev/null @@ -1,51 +0,0 @@ - -
-
-

- array(), - ); - printf( - wp_kses( - /* translators: %s: number of remaining credits */ - __( - 'You are on a free plan with %s compressions left this month.', // WPCS: Needed for proper translation. - 'tiny-compress-images' - ), - $strong - ), - intval( $remaining_credits ) - ); - ?> -

-

- -

-
- - - - - 0 ) { ?> -

- - - -

- -
diff --git a/src/views/bulk-optimization.php b/src/views/bulk-optimization.php index 025dc298..9c91fe55 100644 --- a/src/views/bulk-optimization.php +++ b/src/views/bulk-optimization.php @@ -2,346 +2,182 @@ /** * Bulk optimization view. * - * @var array $admin_colors - * @var array $stats - * @var array $active_tinify_sizes - * @var float $estimated_costs - * @var string $email_address - * @var bool $is_on_free_plan - * @var int $remaining_credits - * @var bool $bulk_running + * @var array $stats See Tiny_Bulk_Optimization::get_optimization_statistics(). + * @var array $widget See Tiny_Dashboard::get_widget_data(). + * @var string $state The widget state, or no_sizes. + * @var int $compression_count Compressions made this month so far. + * @var bool $start_on_load * @var Tiny_Plugin $this */ -$total_sizes = $stats['optimized-image-sizes'] + $stats['available-unoptimized-sizes']; -$settings_url = admin_url( 'options-general.php?page=tinify' ); -$friendly_name = $this->friendly_user_name(); -?> - +$has_library_bar = ! in_array( $state, array( 'pending', 'empty', 'no_sizes' ), true ); +$has_list = count( $items ) > 0 && 'pending' !== $state; +?> +
+

+

+ +

+
+ +
+
+
+ + + +
+
+

+ + ' . esc_html( number_format_i18n( $widget['percentage'] ) ) . '' + ); + ?> + +
+
+ +
+
+ + ' . esc_html( number_format_i18n( $widget['images_optimized'] ) ) . '', + esc_html( number_format_i18n( $widget['images_total'] ) ) + ); + ?> + + + + +
+
+ -
-

-

-
-
-
-
-

-

- '; - require __DIR__ . '/request-review.php'; - } elseif ( $stats['optimized-image-sizes'] > 0 ) { - if ( $percentage_of_files > 75 ) { - /* translators: %s: friendly user name */ - printf( esc_html__( '%s, you are doing great!', 'tiny-compress-images' ), esc_html( $friendly_name ) ); + +

+

+ -

- -

- -

- - - - - - - -
-

- images', 'tiny-compress-images' ), - array( - 'br' => array(), - ) - ) - ?> -

- - - -
-

- image sizes', 'tiny-compress-images' ), - array( - 'br' => array(), - ) - ) - ?> -

- - - -
- -
- 0 && count( $active_tinify_sizes ) > 0 && $stats['available-unoptimized-sizes'] > 0 ) { ?> -

- -

- -

- '; - $active_tinify_sizes_count = count( $active_tinify_sizes ); - for ( $i = 0; $i < $active_tinify_sizes_count; ++$i ) { - $name = $active_tinify_sizes[ $i ]; - if ( '0' == $name ) { - echo '

  • - ' . esc_html__( 'Original image', 'tiny-compress-images' ) . '
  • '; - } else { - echo '
  • - ' . esc_html( ucfirst( $name ) ) . '
  • '; - } - } - echo ''; - } - ?> -

    -

    - 0 ) { ?> - %d size is optimized.', 'For each uploaded image %d sizes are optimized.', count( $active_tinify_sizes ), 'tiny-compress-images' ), - array( - 'strong' => array(), - ) - ), - count( $active_tinify_sizes ) - ) - ?> - - array( - 'href' => array(), - ), - ) - ), - '' . esc_html__( 'here', 'tiny-compress-images' ) . '' - ) - ?> -

    -
    -
    -
    -

    - cost', 'tiny-compress-images' ), - array( - 'br' => array(), - ) - ) - ?> -

    - $ - USD - 0 ) { ?> -
    - -
    -

    - %1$d %2$s a month and you are still on a free account %3$s.', 'tiny-compress-images' ), - array( - 'strong' => array(), - 'a' => array( - 'href' => array(), - ), - ) - ), - (int) Tiny_Config::MONTHLY_FREE_COMPRESSIONS, - esc_html__( 'image sizes', 'tiny-compress-images' ), - '' . esc_html__( ' upgrade here', 'tiny-compress-images' ) . '' - ); - ?> -

    -
    -
    - -
    -
    - array( - 'href' => array(), - ), - ) - ), - '' . esc_html__( 'here', 'tiny-compress-images' ) . '' - ) - ?> + ?> +

    +
      -
      -
      -
      -
      -

      -

      - -

      - -
      - - - - - - - - - - - - - -
      - - - -
      - - - -
      - - - -
      + + + + -
      +
      - $remaining_credits; ?> -
      -
      -
      -
      -
      - - / - - + +
      + settings->render_account_status( true ); ?> + +
      +
      +
      +

      +
      +
      - 0 ) { - require_once __DIR__ . '/bulk-optimization-form.php'; - } - ?> + + settings->render_credits( $can_manage ); ?>
      -
      - - - - - - - - - - - - - - - -
      + diff --git a/src/views/optimization-chart.php b/src/views/optimization-chart.php deleted file mode 100644 index d9cb1931..00000000 --- a/src/views/optimization-chart.php +++ /dev/null @@ -1,55 +0,0 @@ - - - -
      - - - - -
      -
      %
      -
      -
      -
      diff --git a/src/views/request-review.php b/src/views/request-review.php deleted file mode 100644 index 0d43993f..00000000 --- a/src/views/request-review.php +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/views/settings-credits.php b/src/views/settings-credits.php index c1f6f46d..e5f136f0 100644 --- a/src/views/settings-credits.php +++ b/src/views/settings-credits.php @@ -2,6 +2,7 @@ /** * Settings section for showing credit cost per image * + * @var bool $show_settings_link boolean wether to link to the settings page * @var int $tiny_sizes_count number of sizes that are compressed * @var bool $tiny_resize wether or not the original size is resized * @var bool $tiny_wr2x plugin wr2x compressed causes images to be compressed twice @@ -31,5 +32,8 @@
      \ No newline at end of file diff --git a/test/integration/bulkoptimization.spec.ts b/test/integration/bulkoptimization.spec.ts index 369279e5..3a79753b 100644 --- a/test/integration/bulkoptimization.spec.ts +++ b/test/integration/bulkoptimization.spec.ts @@ -47,85 +47,63 @@ test.describe('bulkoptimization', () => { }]); } - test('display upgrade button for account with insufficient credits', async () => { + test('offer an upgrade next to a smaller run for an account with insufficient credits', async () => { await setAPIKey(page, 'INSUFFICIENTCREDITS123'); - await setCompressionTiming(page, 'auto'); - await enableCompressionSizes(page, ['0', 'thumbnail', 'medium']); - - await uploadImages(page, 'input-example.jpg'); + await setCompressionTiming(page, 'manual'); + await enableCompressionSizes(page, ['0']); + await uploadImages(page, 'input-example.jpg', 2, { generateSizes: false }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await expect(page.getByText('Upgrade account')).toBeVisible(); - await expect(page.getByRole('link', { name: 'No thanks, continue anyway' })).toBeVisible(); + await expect(page.getByText('Not enough credits')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Upgrade' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Compress 1 image' })).toBeVisible(); }); - test('not display dismiss link for no credits', async () => { + test('only offer an upgrade for an account without credits', async () => { await setAPIKey(page, 'NOCREDITS123'); - await setCompressionTiming(page, 'auto'); - await enableCompressionSizes(page, ['0', 'thumbnail', 'medium']); - - await uploadImages(page, 'input-example.jpg'); + await setCompressionTiming(page, 'manual'); + await enableCompressionSizes(page, ['0']); + await uploadImages(page, 'input-example.jpg', 1, { generateSizes: false }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await expect(page.getByText('Upgrade account')).toBeVisible(); - await expect(page.getByRole('link', { name: 'No thanks, continue anyway' })).not.toBeVisible(); + await expect(page.getByText('Out of credits')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Upgrade' })).toBeVisible(); + await expect(page.getByRole('button', { name: /^Compress/ })).toHaveCount(0); }); - test('show bulk optimization button after dismissing notice', async () => { - await setAPIKey(page, 'INSUFFICIENTCREDITS123'); - await setCompressionTiming(page, 'auto'); - await enableCompressionSizes(page, ['0', 'thumbnail', 'medium']); - - await uploadImages(page, 'input-example.jpg'); + test('not offer an upgrade to paid accounts', async () => { + await setAPIKey(page, 'PAID123'); + await setCompressionTiming(page, 'manual'); + await enableCompressionSizes(page, ['0']); + await uploadImages(page, 'input-example.jpg', 1, { generateSizes: false }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await page.getByRole('link', { name: 'No thanks, continue anyway' }).click(); - - await expect(page.getByRole('button', { name: 'Start Bulk Optimization' })).toBeVisible(); + await expect(page.getByText('Ready to compress')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Upgrade' })).toHaveCount(0); }); - test('show notice after dismissing notice and refreshing page', async () => { - await setAPIKey(page, 'INSUFFICIENTCREDITS123'); - await setCompressionTiming(page, 'auto'); + test('show the empty library', async () => { await enableCompressionSizes(page, ['0', 'thumbnail', 'medium']); - await uploadImages(page, 'input-example.jpg'); - await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await page.getByRole('link', { name: 'No thanks, continue anyway' }).click(); - - await page.reload(); - - await expect(page.getByRole('button', { name: 'Start Bulk Optimization' })).not.toBeVisible(); - await expect(page.getByRole('link', { name: 'Upgrade account' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'No thanks, continue anyway' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'No images yet' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Upload images' })).toBeVisible(); }); - test('not display upgrade button for paid accounts', async () => { - await setAPIKey(page, 'PAID123'); - - await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - - await expect(page.getByText('Upgrade account')).not.toBeVisible(); - }); - - test('summary should display correct values for empty library', async () => { - await enableCompressionSizes(page, ['0', 'thumbnail', 'medium']); + test('show that no image sizes are selected', async () => { + await setAPIKey(page, 'JPG123'); + await setCompressionTiming(page, 'manual'); + await uploadImages(page, 'input-example.jpg', 1, { generateSizes: false }); + await enableCompressionSizes(page, []); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await expect(page.locator('#uploaded-images')).toHaveText('0'); - await expect(page.locator('#optimizable-image-sizes')).toHaveText('0'); - await expect(page.locator('#estimated-cost')).toHaveText('$ 0.00'); - await expect(page.locator('#optimized-image-sizes')).toHaveText('0'); - await expect(page.locator('#unoptimized-library-size')).toHaveText('-'); - await expect(page.locator('#optimized-library-size')).toHaveText('-'); - await expect(page.locator('#savings-percentage')).toHaveText('0%'); - await expect(page.locator('#compression-progress-bar')).toHaveText('0 / 0 (100%)'); + await expect(page.getByRole('heading', { name: 'No image sizes selected' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Change settings' }).first()).toBeVisible(); }); test('bulk optimize webp images', async () => { @@ -147,9 +125,8 @@ test.describe('bulkoptimization', () => { await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await expect(page.locator('#uploaded-images')).toHaveText('3'); - await expect(page.locator('#optimizable-image-sizes')).toHaveText('5'); - await expect(page.locator('#optimized-image-sizes')).toHaveText('4'); + await expect(page.getByRole('heading', { name: '2 images to compress' })).toBeVisible(); + await expect(page.getByText('1 of 3 compressed')).toBeVisible(); }); test('summary should display correct values', async () => { @@ -167,18 +144,12 @@ test.describe('bulkoptimization', () => { await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - // We uploaded 3 images - await expect(page.locator('#uploaded-images')).toHaveText('3'); - await expect(page.locator('#optimizable-image-sizes')).toHaveText('5'); - await expect(page.locator('#optimized-image-sizes')).toHaveText('4'); - - // Comparing byte sizes is unreliable at the moment. We need to figure out - // why there are differences between environments and versions. - // await expect(page.locator('#unoptimized-library-size')).toHaveText('3.03 MB'); - // await expect(page.locator('#optimized-library-size')).toHaveText('2.36 MB'); - // await expect(page.locator('#savings-percentage')).toHaveText('22.2%'); - - await expect(page.locator('#compression-progress-bar')).toHaveText('4 / 9 (44%)'); + await expect(page.getByRole('heading', { name: '2 images to compress' })).toBeVisible(); + await expect(page.getByText('1 of 3 compressed')).toBeVisible(); + await expect(page.getByText('2 to compress')).toBeVisible(); + await expect(page.getByTestId('bulk-item-status-0')).toHaveText('Waiting'); + await expect(page.getByTestId('bulk-item-status-1')).toHaveText('Waiting'); + await expect(page.getByText('2 waiting')).toBeVisible(); }); test('start bulk optimization should optimize remaining images', async () => { @@ -196,14 +167,13 @@ test.describe('bulkoptimization', () => { await setBulkBehaviour(page, { loopbackSupported: true }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); + await page.getByRole('button', { name: 'Compress 2 images' }).click(); - await expect(page.locator('#optimizable-image-sizes')).toHaveText('5'); - await expect(page.locator('#compression-progress-bar')).toHaveText('4 / 9 (44%)'); - - await page.getByRole('button', { name: 'Start Bulk Optimization' }).click(); - - await expect(page.locator('.optimization-buttons_notice')).toContainText('continues in the background'); - await expect(page.locator('#compression-progress-bar')).toHaveText('9 / 9 (100%)'); + // A short run can be done before the notice is checked, so it may already be hidden. + await expect(page.locator('[data-tiny-bulk="run-notice"]')).toContainText('continues in the background'); + await expect(page.getByRole('heading', { name: 'All 3 images are compressed' })).toBeVisible({ timeout: 15000 }); + await expect(page.getByRole('heading', { name: 'Recently compressed' })).toBeVisible(); + await expect(page.getByTestId('bulk-item-status-0')).toContainText('smaller'); }); test('start bulk optimization without loopback should optimize remaining images in the page', async () => { @@ -221,14 +191,10 @@ test.describe('bulkoptimization', () => { await setBulkBehaviour(page, { loopbackSupported: false }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); + await page.getByRole('button', { name: 'Compress 2 images' }).click(); - await expect(page.locator('#optimizable-image-sizes')).toHaveText('5'); - await expect(page.locator('#compression-progress-bar')).toHaveText('4 / 9 (44%)'); - - await page.getByRole('button', { name: 'Start Bulk Optimization' }).click(); - - await expect(page.locator('.optimization-buttons_notice')).toContainText('keep this page open'); - await expect(page.locator('#compression-progress-bar')).toHaveText('9 / 9 (100%)'); + await expect(page.locator('[data-tiny-bulk="run-notice"]')).toContainText('keep this page open'); + await expect(page.getByRole('heading', { name: 'All 3 images are compressed' })).toBeVisible({ timeout: 15000 }); }); test('background optimization continues when page is refreshed', async () => { @@ -239,16 +205,17 @@ test.describe('bulkoptimization', () => { await setBulkBehaviour(page, { loopbackSupported: true, compressionDuration: 10 }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await page.getByRole('button', { name: 'Start Bulk Optimization' }).click(); - await expect(page.locator('.optimization-buttons_notice')).toContainText('continues in the background'); + await page.getByRole('button', { name: 'Compress 1 image' }).click(); + await expect(page.getByText('continues in the background')).toBeVisible(); await page.reload(); - await expect(page.locator('.optimization-buttons_notice')).toContainText('continues in the background'); - await expect(page.locator('#compression-progress-bar')).toHaveText('1 / 1 (100%)', { timeout: 15000 }); + await expect(page.getByRole('heading', { name: 'Compressing 0 of 1 image' })).toBeVisible(); + await expect(page.getByText('continues in the background')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'All 1 image is compressed' })).toBeVisible({ timeout: 15000 }); }); - test('cancel will clear the queue', async () => { + test('pause returns to the images that are left', async () => { await setAPIKey(page, 'JPG123'); await setCompressionTiming(page, 'manual'); await enableCompressionSizes(page, ['0']); @@ -256,17 +223,16 @@ test.describe('bulkoptimization', () => { await setBulkBehaviour(page, { loopbackSupported: false, compressionDuration: 5 }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await page.getByRole('button', { name: 'Start Bulk Optimization' }).click(); - await expect(page.locator('.optimization-buttons_notice')).toContainText('keep this page open'); + await page.getByRole('button', { name: 'Compress 6 images' }).click(); + await expect(page.getByText('keep this page open')).toBeVisible(); - // The cursor still rests where Start was clicked, so Optimizing can already have turned into Cancel. - await page.locator('#id-optimizing, #id-cancel').filter({ visible: true }).hover(); - await page.locator('#id-cancel').click(); + await page.getByRole('button', { name: 'Pause' }).click(); + await expect(page.getByRole('button', { name: 'Pausing...' })).toBeDisabled(); - // Images that were being compressed finish, the others are cancelled. - await expect(page.locator('#bulk-optimization-actions')).toBeHidden({ timeout: 15000 }); - await expect(page.locator('[data-status="cancelled"]').first()).toHaveText('Cancelled'); - await expect(page.getByText('All images are processed')).not.toBeVisible(); + // Images that were being compressed finish, then the page shows what is left. + await expect(page.getByText('Ready to compress')).toBeVisible({ timeout: 20000 }); + await expect(page.getByRole('heading', { name: /^\d images? to compress$/ })).toBeVisible(); + await expect(page.getByTestId('bulk-item-status-0')).toHaveText('Waiting'); }); test('stop when the account runs out of credits', async () => { @@ -276,17 +242,11 @@ test.describe('bulkoptimization', () => { await uploadImages(page, 'input-example.jpg', 6, { generateSizes: false }); await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await page.getByRole('button', { name: 'Start Bulk Optimization' }).click(); - - // The first image to hit the limit takes the rest off the queue. - await expect(page.getByText('Optimization stopped early.')).toBeVisible({ timeout: 15000 }); - await expect(page.locator('[data-status="cancelled"]').first()).toHaveText('Stopped'); - }); + await page.getByRole('button', { name: 'Compress 6 images' }).click(); + await expect(page.getByText('Compressing', { exact: true })).toBeVisible(); - test('should display tooltips', async () => { - await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - const tooltips = await page.locator('div.tip').all(); - await expect(tooltips.length).toEqual(1); + // The first image to hit the limit takes the rest off the queue, and the page shows what is left. + await expect(page.getByRole('heading', { name: '6 images to compress' })).toBeVisible({ timeout: 30000 }); }); test('will only apply a conversion when images are already compressed but not converted', async ({ page }) => { @@ -305,23 +265,10 @@ test.describe('bulkoptimization', () => { // apply bulk optimization await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization'); - await Promise.all([ - page.waitForResponse(res => - res.url().includes('admin-ajax.php') && res.status() === 200 - ), - page.locator('#id-start').click(), - ]); - - // we are expecting 2 unoptimized images sizes = 2 conversion and 1 compression - // expect two rows in the optimization items table - const optimizationRows = page.locator('#optimization-items tbody tr'); - await expect(optimizationRows).toHaveCount(2); - - // first row should have "1 compressed and 1 converted" - // second row should have "1 converted" - await expect(page.getByTestId(`bulk-item-status-1`)).toContainText('1 compressed 1 converted'); - await expect(page.getByTestId(`bulk-item-status-0`)).toContainText('1 converted'); - - // this should have cost us 3 credits + await page.getByRole('button', { name: 'Compress 2 images' }).click(); + + // both images end up optimized: one only converted, the other compressed and converted + await expect(page.getByRole('heading', { name: 'All 2 images are compressed' })).toBeVisible({ timeout: 15000 }); + await expect(page.locator('.tiny-bulk-item[data-status="compressed"]')).toHaveCount(2); }); }); diff --git a/test/integration/dashboardwidget.spec.ts b/test/integration/dashboardwidget.spec.ts index c5c0d525..b0671a31 100644 --- a/test/integration/dashboardwidget.spec.ts +++ b/test/integration/dashboardwidget.spec.ts @@ -94,7 +94,7 @@ test.describe('dashboardwidget', () => { await widget().getByRole('link', { name: 'Compress 1 image' }).click(); await expect(page).toHaveURL(/page=tiny-bulk-optimization$/); - await expect(page.locator('#compression-progress-bar')).toHaveText('1 / 1 (100%)', { timeout: 15000 }); + await expect(page.getByRole('heading', { name: 'All 1 image is compressed' })).toBeVisible({ timeout: 15000 }); }); test('start flag without a nonce does not start the bulk optimization', async () => { @@ -105,8 +105,8 @@ test.describe('dashboardwidget', () => { await page.goto('/wp-admin/upload.php?page=tiny-bulk-optimization&start=1'); await expect(page).toHaveURL(/page=tiny-bulk-optimization$/); - await expect(page.locator('#id-start')).toBeVisible(); - await expect(page.locator('#compression-progress-bar')).toHaveText('0 / 1 (0%)'); + await expect(page.getByRole('button', { name: 'Compress 1 image' })).toBeVisible(); + await expect(page.getByTestId('bulk-item-status-0')).toHaveText('Waiting'); }); test('paid accounts pay per compression after their free credits', async () => { diff --git a/test/unit/TinyBulkOptimizationTest.php b/test/unit/TinyBulkOptimizationTest.php index 505b40dd..742150a6 100644 --- a/test/unit/TinyBulkOptimizationTest.php +++ b/test/unit/TinyBulkOptimizationTest.php @@ -209,18 +209,24 @@ public function test_get_optimization_statistics() $this->assertEquals( array( 'uploaded-images' => 3, - 'optimized-image-sizes' => 3, - 'available-unoptimized-sizes' => 6, 'optimized-library-size' => 529136, 'unoptimized-library-size' => 676136, 'available-for-optimization' => array( array( 'ID' => 1, 'post_title' => 'I am uncompressed and unconverted', + 'file' => 'image-4350.png', + 'sizes' => 3, + 'bytes' => 233568, + 'thumbnail' => '/wp-content/uploads/2023/05/image-4350-150x150.png', ), array( 'ID' => 4350, 'post_title' => 'I am compressed but not converted', + 'file' => 'image-4351.jpg', + 'sizes' => 3, + 'bytes' => 223568, + 'thumbnail' => '/wp-content/uploads/2023/05/image-4351-150x150.jpg', ), ), 'display-percentage' => 21.7, diff --git a/test/unit/TinyCompressTest.php b/test/unit/TinyCompressTest.php deleted file mode 100644 index a2488e65..00000000 --- a/test/unit/TinyCompressTest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertEqualsWithDelta( - 150 * 0, - Tiny_Compress::estimate_cost(150, 0), - 0.001 - ); - } - - public function test_estimate_cost_normal_and_free() - { - $this->assertEqualsWithDelta( - 350 * 0 + 2650 * 0.009, - Tiny_Compress::estimate_cost(3000, 150), - 0.001 - ); - } - - public function test_estimate_cost_cheap_and_normal_and_free() - { - $this->assertEqualsWithDelta( - 500 * 0 + 9500 * 0.009 + 40000 * 0.002, - Tiny_Compress::estimate_cost(50000, 0), - 0.001 - ); - } -} diff --git a/test/unit/TinyPluginAjaxTest.php b/test/unit/TinyPluginAjaxTest.php index 562247aa..53a1c6ed 100644 --- a/test/unit/TinyPluginAjaxTest.php +++ b/test/unit/TinyPluginAjaxTest.php @@ -37,6 +37,26 @@ public function test_bulk_queue_handlers_refuse_a_user_without_permission_with_a $this->assert_bulk_queue_handlers_refuse_with_a_403(); } + public function test_bulk_queue_status_includes_the_compression_count() { + $this->wp->addOption( 'tinypng_status', 42 ); + $this->wp->addMethod( 'update_meta_cache' ); + $this->wp->stub( 'check_ajax_referer', function () { + return true; + } ); + $this->wp->stub( 'current_user_can', function () { + return true; + } ); + $response = null; + $this->wp->addMethod( 'wp_send_json_success' ); + $this->wp->stub( 'wp_send_json_success', function ( $data ) use ( &$response ) { + $response = $data; + } ); + + $this->subject->ajax_bulk_queue_status(); + + $this->assertEquals( 42, $response['compression_count'] ); + } + private function assert_bulk_queue_handlers_refuse_with_a_403() { $this->wp->addMethod( 'status_header' ); $this->wp->addMethod( 'wp_die' ); diff --git a/test/unit/TinyPluginTest.php b/test/unit/TinyPluginTest.php index 5971aaf8..190fee16 100644 --- a/test/unit/TinyPluginTest.php +++ b/test/unit/TinyPluginTest.php @@ -261,42 +261,6 @@ public function test_wrong_metadata_should_save_tiny_metadata() $this->assertEquals(2, count($this->wp->getCalls('update_post_meta'))); } - public function test_get_bulk_cost() - { - $virtual_compressed_image = array( - 'path' => '2015/09', - 'images' => array( - array( - "file" => "uncompressed.png", - "size" => 137856, - ), - ) - ); - $this->wp->createImagesFromJSON($virtual_compressed_image); - - $wpdb_results = array( - array( - 'ID' => 1, - 'post_title' => 'Uncompressed Image', - 'meta_value' => serialize(array( - 'width' => 1256, - 'height' => 1256, - 'file' => '2015/09/uncompressed.png', - 'sizes' => array() - )), - 'tiny_meta_value' => '' - ), - ); - - $stats = Tiny_Bulk_Optimization::get_optimization_statistics(new Tiny_Settings(), $wpdb_results); - $free_limit = 500; - $cost = Tiny_Compress::estimate_cost( - $stats['available-unoptimized-sizes'], - $free_limit, - ); - - $this->assertEquals($cost, 0.01, 0.0001, 'one compression is $0,009, rounded to 0.01',); - } public function test_when_image_is_uncompressed_and_conversion_enabled_cost_two_credits() { @@ -328,25 +292,8 @@ public function test_when_image_is_uncompressed_and_conversion_enabled_cost_two_ ), ); - // Mock settings with compression count - $mock_settings = $this->createMock(Tiny_Settings::class); - $mock_settings->method('get_conversion_enabled')->willReturn(true); - $mock_settings->method('get_compression_count')->willReturn(500); - - $tiny_plugin = new Tiny_Plugin(); - - // because settings is private we need reflection - $ref = new \ReflectionClass($tiny_plugin); - $settings_prop = $ref->getProperty('settings'); - $settings_prop->setAccessible(true); - $settings_prop->setValue($tiny_plugin, $mock_settings); - $stats = Tiny_Bulk_Optimization::get_optimization_statistics(new Tiny_Settings(), $wpdb_results); $this->assertEquals($stats['estimated_credit_use'], 2, 'one uncompressed image that will be converted is 2 credits'); - - $cost = $tiny_plugin->get_estimated_bulk_cost($stats['estimated_credit_use']); - - $this->assertEquals($cost, 0.02, 0.0001, 'a conversion will cost 2 credits at $0.009 each when 500 compressions already used'); } /** @@ -405,23 +352,8 @@ public function test_when_files_is_compressed_will_only_cost_1_credit() ), ); - // Mock settings with compression count - $mock_settings = $this->createMock(Tiny_Settings::class); - $mock_settings->method('get_conversion_enabled')->willReturn(true); - $mock_settings->method('get_compression_count')->willReturn(500); - - $tiny_plugin = new Tiny_Plugin(); - - // because settings is private we need reflection - $ref = new \ReflectionClass($tiny_plugin); - $settings_prop = $ref->getProperty('settings'); - $settings_prop->setAccessible(true); - $settings_prop->setValue($tiny_plugin, $mock_settings); - $stats = Tiny_Bulk_Optimization::get_optimization_statistics(new Tiny_Settings(), $wpdb_results); - $cost = $tiny_plugin->get_estimated_bulk_cost($stats['estimated_credit_use']); - - $this->assertEquals($cost, 0.01, 0.0001, 'a compressed image that will be converted will cost 1 credit at $0.009 (rounded $0.01) each when 500 compressions already used'); + $this->assertEquals($stats['estimated_credit_use'], 1, 'a compressed image that will be converted costs 1 credit'); } public function test_conversion_enabled_but_filtered_off_not_load_picture() diff --git a/test/unit/TinySettingsAdminTest.php b/test/unit/TinySettingsAdminTest.php index 308d93f1..9076841b 100644 --- a/test/unit/TinySettingsAdminTest.php +++ b/test/unit/TinySettingsAdminTest.php @@ -586,4 +586,26 @@ public function test_should_count_credits_per_image_for_each_size() { public function test_should_count_credits_per_image_with_resize_and_conversion() { $this->assertEquals( 11, Tiny_Settings::get_credits_per_image( 5, true, false, true ) ); } + + public function test_render_credits_links_to_settings_when_asked() { + $this->assertStringContainsString( 'Change settings', $this->render_credits( true ) ); + } + + public function test_render_credits_does_not_link_to_settings_by_default() { + $output = $this->render_credits( false ); + $this->assertStringContainsString( 'How credits are used', $output ); + $this->assertStringNotContainsString( 'Change settings', $output ); + } + + private function render_credits( $show_settings_link ) { + $this->wp->addMethod( '_n' ); + $this->wp->stub( '_n', function ( $single, $plural, $number ) { + return 1 === $number ? $single : $plural; + } ); + $this->wp->addMethod( 'number_format_i18n' ); + $this->wp->addMethod( 'esc_url' ); + ob_start(); + $this->subject->render_credits( $show_settings_link ); + return ob_get_clean(); + } } diff --git a/test/unit/Tiny_Dashboard_Test.php b/test/unit/Tiny_Dashboard_Test.php index 71c26962..f0f4b7e8 100644 --- a/test/unit/Tiny_Dashboard_Test.php +++ b/test/unit/Tiny_Dashboard_Test.php @@ -35,8 +35,6 @@ private function stats($uploaded, $remaining, $unoptimized_size = 0, $optimized_ { return array( 'uploaded-images' => $uploaded, - 'optimized-image-sizes' => 0, - 'available-unoptimized-sizes' => 0, 'optimized-library-size' => $optimized_size, 'unoptimized-library-size' => $unoptimized_size, 'estimated_credit_use' => $credit_use,