From e7cdf578ced87cb6f7bb4c84a9d1660da8b6f376 Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 23 Sep 2026 09:22:33 +0200 Subject: [PATCH 01/18] update dashboard widget --- src/class-tiny-dashboard.php | 111 ++++++++++++++++++ src/class-tiny-plugin.php | 55 +-------- src/css/admin.css | 4 + src/css/dashboard-widget.css | 179 ++++++++++++++---------------- src/js/dashboard-widget.js | 125 --------------------- src/views/dashboard-widget.php | 167 ++++++++++++---------------- src/views/progress-circle.php | 76 +++++++++++++ test/unit/Tiny_Dashboard_Test.php | 84 ++++++++++++++ tiny-compress-images.php | 1 + 9 files changed, 434 insertions(+), 368 deletions(-) create mode 100644 src/class-tiny-dashboard.php delete mode 100644 src/js/dashboard-widget.js create mode 100644 src/views/progress-circle.php create mode 100644 test/unit/Tiny_Dashboard_Test.php diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php new file mode 100644 index 00000000..79b77d07 --- /dev/null +++ b/src/class-tiny-dashboard.php @@ -0,0 +1,111 @@ +settings = $settings; + } + + public function admin_init() { + add_action( + 'wp_dashboard_setup', + $this->get_method( 'add_dashboard_widget' ) + ); + } + + public function add_dashboard_widget() { + wp_enqueue_style( + self::NAME . '_dashboard_widget', + plugins_url( '/css/dashboard-widget.css', __FILE__ ), + array(), + self::wp_version(), + ); + + wp_add_dashboard_widget( + $this->get_prefixed_name( 'dashboard_widget' ), + esc_html__( 'TinyPNG - JPEG, PNG & WebP image compression', 'tiny-compress-images' ), + $this->get_method( 'add_widget_view' ) + ); + } + + public function add_widget_view() { + $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); + $widget = self::get_widget_data( $optimization_statistics ); + include __DIR__ . '/views/dashboard-widget.php'; + } + + /** + * Logic for the view + * + * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). + * @return array + */ + public static function get_widget_data( $optimization_stats ) { + $images_remaining = count( $optimization_stats['available-for-optimization'] ); + $bytes_total = $optimization_stats['unoptimized-library-size']; + $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); + + $images_optimized = $images_total - $images_remaining; + $images_optimized = max( 0, intval( $images_optimized ) ); + $images_remaining = max( 0, $images_total - $images_optimized ); + $bytes_saved = $bytes_total - $optimization_stats['optimized-library-size']; + + $percentage = $images_total > 0 + ? intval( floor( $images_optimized / $images_total * 100 ) ) + : 0; + $percentage = max( 0, min( 100, $percentage ) ); + + if ( 0 === $images_total ) { + $status = 'empty'; + $label = ''; + $panda = 'panda-waiting.png'; + } elseif ( 0 === $images_remaining ) { + $status = 'done'; + $label = __( 'all done', 'tiny-compress-images' ); + $panda = 'panda-laying.png'; + } else { + $status = 'in_progress'; + $label = $percentage >= 50 + ? __( 'almost there', 'tiny-compress-images' ) + : __( 'keep going', 'tiny-compress-images' ); + $panda = 'panda-eating.png'; + } + + return array( + 'status' => $status, + 'images_optimized' => $images_optimized, + 'images_total' => $images_total, + 'images_remaining' => $images_remaining, + 'bytes_saved' => max( 0, intval( $bytes_saved ) ), + 'percentage' => $percentage, + 'label' => $label, + 'panda' => $panda, + ); + } +} diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 4c567bf7..0fc7ef7c 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -42,6 +42,7 @@ public function __construct() { parent::__construct(); $this->settings = new Tiny_Settings(); new Tiny_Conversion( $this->settings ); + new Tiny_Dashboard( $this->settings ); } public function set_compressor( $compressor ) { @@ -157,11 +158,6 @@ public function ajax_init() { } public function admin_init() { - add_action( - 'wp_dashboard_setup', - $this->get_method( 'add_dashboard_widget' ) - ); - add_action( 'admin_enqueue_scripts', $this->get_method( 'enqueue_scripts' ) @@ -819,55 +815,6 @@ public function render_bulk_optimization_page() { include __DIR__ . '/views/bulk-optimization.php'; } - public function add_dashboard_widget() { - wp_enqueue_style( - self::NAME . '_chart', - plugins_url( '/css/optimization-chart.css', __FILE__ ), - array(), - self::version() - ); - - wp_enqueue_style( - self::NAME . '_dashboard_widget', - plugins_url( '/css/dashboard-widget.css', __FILE__ ), - array(), - self::version() - ); - - wp_register_script( - self::NAME . '_dashboard_widget', - plugins_url( '/js/dashboard-widget.js', __FILE__ ), - array(), - self::version(), - true - ); - - /* - This might be deduplicated with the admin script localization, but - the order of including scripts is sometimes different. So in that - case we need to make sure that the order of inclusion is correct. */ - wp_localize_script( - self::NAME . '_dashboard_widget', - 'tinyCompressDashboard', - array( - 'nonce' => wp_create_nonce( 'tiny-compress' ), - ) - ); - - wp_enqueue_script( self::NAME . '_dashboard_widget' ); - - wp_add_dashboard_widget( - $this->get_prefixed_name( 'dashboard_widget' ), - esc_html__( 'TinyPNG - JPEG, PNG & WebP image compression', 'tiny-compress-images' ), - $this->get_method( 'add_widget_view' ) - ); - } - - public function add_widget_view() { - $admin_colors = self::retrieve_admin_colors(); - include __DIR__ . '/views/dashboard-widget.php'; - } - private static function retrieve_admin_colors() { global $_wp_admin_css_colors; $admin_colour_scheme = get_user_option( 'admin_color', get_current_user_id() ); diff --git a/src/css/admin.css b/src/css/admin.css index bad18a87..40b2a869 100644 --- a/src/css/admin.css +++ b/src/css/admin.css @@ -1,3 +1,7 @@ +:root { + --tiny-color-text-muted: #6a7282 +} + div.misc-pub-section.tiny-compress-images { border-top: 1px solid #e1e1e1; padding-bottom: 16px; diff --git a/src/css/dashboard-widget.css b/src/css/dashboard-widget.css index bf4b9a80..7e0d3ae8 100644 --- a/src/css/dashboard-widget.css +++ b/src/css/dashboard-widget.css @@ -1,129 +1,118 @@ -div.tiny_dashboard_widget div.inside { - min-height: 200px; +#tinypng_dashboard_widget .inside { margin: 0; - padding-bottom: 0; - overflow: hidden; + padding: 0; } -div.tiny_dashboard_widget div.inside div.spinner { - position: absolute; - visibility: visible; - z-index: 10; - top: 30%; - left: 50%; - margin-left: -12px; +.tiny-widget { + color: var(--tiny-color-text-muted); + font-size: 13px; + line-height: 20px; } -div.tiny_dashboard_widget div.inside div.sky { - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 100%; - opacity: 0.3; - background: linear-gradient(180deg, #c6dde4 0%, #fefeff 100%); - /* ie6-9 */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#33C6DDE4', endColorstr='#33FEFEFF',GradientType=0 ); +.tiny-widget-bulk { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + padding: 20px; + text-align: center; } -div.tiny_dashboard_widget div.inside div.cloud { - position: absolute; - right: 0; - top: 0; - height: 240px; - width: 298px; - background-image: url("../images/cloud.png"); - background-size: cover; +.tiny-widget-bulk strong { + color: #1e1e1e; } -div.tiny_dashboard_widget div.inside div.grass { - position: absolute; - right: 0; - bottom: 0; - height: 24px; - width: 536px; - background-image: url("../images/grass.png"); - background-size: cover; +a.tiny-widget-bulk-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + width: 209px; + padding: 15px 0; + background: #3858e9; + color: #fff; + font-size: 15px; + font-weight: bold; + line-height: normal; + text-decoration: none; } -div.tiny_dashboard_widget div.inside div.panda { - position: absolute; - bottom: 0; - background-repeat: no-repeat; - -webkit-background-size: 100%; - background-size: cover; - background-position: bottom; +a.tiny-widget-bulk-button:hover, +a.tiny-widget-bulk-button:focus { + background: #2145e6; + color: #fff; } -div.tiny_dashboard_widget.full-optimized div.inside div.panda { - right: 10%; - width: 305px; - height: 105.5px; - margin-right: 0; - background-image: url("../images/panda-laying.png"); +.tiny-widget-body { + display: flex; + align-items: flex-end; + justify-content: space-between; } -div.tiny_dashboard_widget.half-optimized div.inside div.panda { - right: -230px; - width: 255px; - height: 203px; - margin-right: 45%; - background-image: url("../images/panda-eating.png"); +.tiny-widget-main { + display: flex; + flex-direction: column; + align-self: stretch; + justify-content: space-between; + min-width: 0; } -div.tiny_dashboard_widget.not-optimized div.inside div.panda, -div.tiny_dashboard_widget.no-images-uploaded div.inside div.panda { - right: -196px; - width: 241px; - height: 203px; - margin-right: 45%; - background-image: url("../images/panda-waiting.png"); +.tiny-widget-progress { + display: flex; + align-items: center; + gap: 20px; } -div.tiny_dashboard_widget.no-images-uploaded div.inside div.no-images, -div.tiny_dashboard_widget.not-optimized div.inside div.not-optimized, -div.tiny_dashboard_widget.half-optimized div.inside div.half-optimized, -div.tiny_dashboard_widget.full-optimized div.inside div.full-optimized { - display: block !important; - position: relative; - z-index: 1; +.tiny-widget-progress, +.tiny-widget-main > .tiny-widget-stats { + padding: 20px; } -div.tiny_dashboard_widget div.inside div#optimization-chart { - margin-top: 12px; - margin-bottom: 40px; - min-width: 160px; +.tiny-widget-stats { + display: flex; + flex-direction: column; + gap: 5px; + line-height: 17px; } -div.tiny_dashboard_widget.full-optimized div.inside.mobile div#optimization-chart { - width: 100%; - margin-top: -5px; - margin-bottom: 95px; +.tiny-widget-title { + color: #030712; + font-size: 16px; } -.ie8 div.tiny_dashboard_widget div.inside div.panda { - background: none; +.tiny-widget-nav { + display: flex; + align-items: center; + padding: 10px 20px; + font-size: 12px; + font-weight: bold; + line-height: 17px; } -.ie8 div.tiny_dashboard_widget div.inside div.grass { - width: 100%; - background-image: url("../images/grass-ie8.png"); - background-repeat: no-repeat; - background-position: bottom; +.tiny-widget-nav a { + color: #3858e9; + text-decoration: none; } -.ie8 div.tiny_dashboard_widget div.inside div#optimization-chart { - display: none; +.tiny-widget-nav a + a { + margin-left: 10px; + padding-left: 10px; + border-left: 1px solid #ccc; } -.ie8 div.tiny_dashboard_widget.no-images-uploaded div.inside div#ie8-compressed, -.ie8 div.tiny_dashboard_widget.not-optimized div.inside div#ie8-compressed, -.ie8 div.tiny_dashboard_widget.half-optimized div.inside div#ie8-compressed, -.ie8 div.tiny_dashboard_widget.full-optimized div.inside div#ie8-compressed { - display: block !important; - position: relative; - z-index: 1; +.tiny-widget-panda { + flex-shrink: 0; + width: 213px; + height: auto; + padding: 0 12px; } -#tinypng_dashboard_widget > button.handlediv { - margin-top: 0; +.tiny-widget-panda-eating { + transform: scaleX(-1); +} + +@media (max-width: 480px) { + .tiny-widget-panda { + display: none; + } } diff --git a/src/js/dashboard-widget.js b/src/js/dashboard-widget.js deleted file mode 100644 index 0bd39aa1..00000000 --- a/src/js/dashboard-widget.js +++ /dev/null @@ -1,125 +0,0 @@ -(function() { - function generateDashboardWidget(element) { - element = jQuery(element); - var container = element.find('.inside'); - jQuery('.chart').addClass('hidden'); - // Adding a class to the widget element so that classes are only used in the stylesheet - jQuery('#tinypng_dashboard_widget').addClass('tiny_dashboard_widget'); - attachHandlers(container); - retrieveStats(container); - } - - function retrieveStats(container) { - jQuery.ajax({ - url: ajaxurl, - type: 'POST', - data: { - _nonce: tinyCompressDashboard.nonce, - action: 'tiny_get_optimization_statistics', - id: '#tinypng_dashboard_widget' - }, - success: function(data) { - if (data === 0) { - container.append('

An error occured.

'); - } else { - renderWidget(data); - } - }, - error: function() { - container.append('

An error occured.

'); - } - }); - } - - function attachHandlers(container) { - setContainerClass(container); - jQuery(window).resize(function(){ - setContainerClass(container); - }); - jQuery('#tinypng_dashboard_widget .hndle').click(function() { - jQuery(this).siblings('.inside').removeClass('mobile'); - }); - } - - function setContainerClass(container) { - if (jQuery(container).width() < 400) { - jQuery(container).addClass('mobile'); - } else if (jQuery(container).width() < 490 && jQuery(container).width() >= 400) { - jQuery(container).addClass('tablet'); - jQuery(container).removeClass('mobile'); - } else if (jQuery(container).width() >= 490) { - jQuery(container).removeClass('tablet').removeClass('mobile'); - } - } - - function renderWidget(data) { - var stats = jQuery.parseJSON(data); - if (stats !== null) { - var savings = stats['display-percentage']; - var libraryOptimized = optimizedPercentage(stats); - renderContent(libraryOptimized, stats, savings); - renderChart(savings); - jQuery('#optimization-chart').show(); - } - jQuery('#widget-spinner').attr('class', 'hidden'); - } - - function renderPercentage(percentage) { - jQuery('#savings-percentage').find('span').html(percentage); - } - - function renderContent(percentage, stats, savingsPercentage) { - renderPercentage(savingsPercentage); - if ( 0 === stats['uploaded-images'] + stats['available-unoptimized-sizes'] ) { - jQuery('#tinypng_dashboard_widget').addClass('no-images-uploaded'); - } else if ( percentage === 0 ) { - jQuery('#tinypng_dashboard_widget').addClass('not-optimized'); - } else if ( percentage === 100 ) { - jQuery('#tinypng_dashboard_widget').addClass('full-optimized'); - } else { - jQuery('#uploaded-images').html( stats['uploaded-images'] ); - jQuery('#unoptimized-sizes').html( stats['available-unoptimized-sizes'] ); - jQuery('#tinypng_dashboard_widget').addClass('half-optimized'); - } - jQuery('#ie8-compressed').find('span').html(savingsPercentage); - } - - function chartOptions(percentage) { - var chart = {}; - chart.size = 160; - chart.radius = chart.size / 2 * 0.9; - chart['main-radius'] = chart.radius * 0.88; - chart['circle-size'] = 2 * Math.PI * chart['main-radius']; - chart['dash-array-size'] = percentage / 100 * chart['circle-size']; - return chart; - } - - function optimizedPercentage(stats) { - if ( 0 !== stats['unoptimized-library-size'] ) { - return Math.round((stats['optimized-image-sizes'] / (stats['optimized-image-sizes'] + stats['available-unoptimized-sizes']) * 100), 0); - } else { - return 0; - } - } - - function renderChart(savingsPercentage) { - var chart = chartOptions(savingsPercentage); - jQuery('#optimization-chart svg circle.main').css('stroke-dasharray', chart['dash-array-size'] + ' ' + chart['circle-size']); - var style = - ' @keyframes shwoosh {' + - ' from { stroke-dasharray: 0 ' + chart['circle-size'] + '}' + - ' to { stroke-dasharray:' + chart['dash-array-size'] + ' ' + chart['circle-size'] + '}}'; - - // JQuery bug where you cannot append to style tag https://bugs.jquery.com/ticket/9832 - try { - jQuery('#tinypng_dashboard_widget .inside style').append(style); - } catch(err) { - - } - } - - // Check if widget is loaded - if (jQuery('#tinypng_dashboard_widget').length) { - generateDashboardWidget('#tinypng_dashboard_widget'); - } -}).call(); diff --git a/src/views/dashboard-widget.php b/src/views/dashboard-widget.php index 5cc866b6..43cfc7b4 100644 --- a/src/views/dashboard-widget.php +++ b/src/views/dashboard-widget.php @@ -3,102 +3,81 @@ /** * Dashboard Widget * - * @var array $admin_colors + * @var array $widget See Tiny_Dashboard::get_widget_data(). * */ -$link = '' . esc_html__( 'bulk optimization', 'tiny-compress-images' ) . ''; +$percentage = $widget['percentage']; +$label = $widget['label']; + +$bulk_url = admin_url('upload.php?page=tiny-bulk-optimization'); +$settings_url = admin_url('options-general.php?page=tinify'); ?> - -
-
-
-
-
-
-

-
-
-

- friendly_user_name() ) ); - echo ' '; - printf( - wp_kses( - /* translators: %s: bulk optimization page */ - __( 'If you like you can to optimize your whole library in one go with the %s page.', 'tiny-compress-images' ), - array( - 'a' => array( - 'href' => array(), - ), - ) - ), - wp_kses( $link, array( 'a' => array( 'href' => array() ) ) ) - ); - ?> -

-
-
-

- friendly_user_name() ) ); - echo ' '; - /* translators: %s: number of optimizable sizes and number of uploaded images */ - printf( esc_html__( 'With your current settings you can still optimize %1$s image sizes from your %2$s uploaded JPEG, PNG, and WebP images.', 'tiny-compress-images' ), '', '' ); - echo ' '; - printf( - wp_kses( - /* translators: %s: bulk optimization link */ - __( 'Start the %s to optimize the remainder of your library.', 'tiny-compress-images' ), - array( - 'a' => array( - 'href' => array(), - ), - ) - ), - wp_kses( $link, array( 'a' => array( 'href' => array() ) ) ) - ); - ?> -

-
-
-

- friendly_user_name() ) ); - echo ' '; - require __DIR__ . '/request-review.php'; - ?> -

-
-
-

- saved %s of your media library size.', 'tiny-compress-images' ), - array( - 'span' => array(), - 'strong' => array(), - ) - ), - '%' - ); - ?> -

-
+
+ +
+ + + + + + + %s images.', 'tiny-compress-images'), array('strong' => array())), + esc_html(number_format_i18n($widget['images_remaining'])) + ); + ?> + +
+ + +
+
+ +
+ + +
+ +
+ +
+ + + + + + +
+
+ - + +
+ + +
+
diff --git a/src/views/progress-circle.php b/src/views/progress-circle.php new file mode 100644 index 00000000..687d3b14 --- /dev/null +++ b/src/views/progress-circle.php @@ -0,0 +1,76 @@ + + + +
+ +
+
%
+ +
+ +
+
diff --git a/test/unit/Tiny_Dashboard_Test.php b/test/unit/Tiny_Dashboard_Test.php new file mode 100644 index 00000000..d98236cc --- /dev/null +++ b/test/unit/Tiny_Dashboard_Test.php @@ -0,0 +1,84 @@ + $uploaded, + 'optimized-image-sizes' => 0, + 'available-unoptimized-sizes' => 0, + 'optimized-library-size' => $optimized_size, + 'unoptimized-library-size' => $unoptimized_size, + 'estimated_credit_use' => 0, + 'available-for-optimization' => array_fill(0, $remaining, array('ID' => 1, 'post_title' => 'image')), + 'display-percentage' => 0, + )); + } + + public function test_is_empty_when_there_are_no_images() + { + $data = $this->widget_data(0, 0); + + $this->assertSame('empty', $data['status']); + $this->assertSame(0, $data['percentage']); + $this->assertSame(0, $data['images_remaining']); + $this->assertSame('panda-waiting.png', $data['panda']); + } + + public function test_is_in_progress_when_images_remain() + { + $data = $this->widget_data(1250, 330); + + $this->assertSame('in_progress', $data['status']); + $this->assertSame(920, $data['images_optimized']); + $this->assertSame(1250, $data['images_total']); + $this->assertSame(330, $data['images_remaining']); + $this->assertSame(73, $data['percentage']); + $this->assertSame('panda-eating.png', $data['panda']); + } + + public function test_is_done_when_no_images_remain() + { + $data = $this->widget_data(1250, 0); + + $this->assertSame('done', $data['status']); + $this->assertSame(1250, $data['images_optimized']); + $this->assertSame(100, $data['percentage']); + $this->assertSame('all done', $data['label']); + $this->assertSame('panda-laying.png', $data['panda']); + } + + public function test_labels_below_half_as_keep_going() + { + $this->assertSame('keep going', $this->widget_data(100, 51)['label']); + } + + public function test_labels_half_and_above_as_almost_there() + { + $this->assertSame('almost there', $this->widget_data(100, 50)['label']); + } + + public function test_does_not_show_100_percent_while_images_remain() + { + $data = $this->widget_data(1000, 1); + + $this->assertSame('in_progress', $data['status']); + $this->assertSame(99, $data['percentage']); + } + + public function test_bytes_saved_is_the_library_size_reduction() + { + $this->assertSame(2000, $this->widget_data(10, 3, 5000, 3000)['bytes_saved']); + } + + public function test_bytes_saved_is_never_negative() + { + $this->assertSame(0, $this->widget_data(10, 3, 3000, 5000)['bytes_saved']); + } +} diff --git a/tiny-compress-images.php b/tiny-compress-images.php index 37ccc7e0..daa13a5e 100644 --- a/tiny-compress-images.php +++ b/tiny-compress-images.php @@ -27,6 +27,7 @@ require dirname( __FILE__ ) . '/src/class-tiny-cli.php'; require dirname( __FILE__ ) . '/src/class-tiny-conversion.php'; require dirname( __FILE__ ) . '/src/class-tiny-picture.php'; +require dirname( __FILE__ ) . '/src/class-tiny-dashboard.php'; require dirname( __FILE__ ) . '/src/class-tiny-apache-rewrite.php'; require dirname( __FILE__ ) . '/src/compatibility/wpml/class-tiny-wpml.php'; require dirname( __FILE__ ) . '/src/compatibility/as3cf/class-tiny-as3cf.php'; From 07a033d1252dd86ac0c08cc46ce6dc103a117d3b Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 23 Sep 2026 14:44:08 +0200 Subject: [PATCH 02/18] refactor ui for dashboard widget --- src/class-tiny-dashboard.php | 69 +++++++++++--- src/class-tiny-settings.php | 4 + src/css/dashboard-widget.css | 147 ++++++++++++++++++------------ src/views/dashboard-widget.php | 134 +++++++++++++++++---------- src/views/progress-circle.php | 15 ++- test/unit/Tiny_Dashboard_Test.php | 63 ++++++++++++- 6 files changed, 306 insertions(+), 126 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 79b77d07..98552766 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -19,6 +19,14 @@ */ class Tiny_Dashboard extends Tiny_WP_Base { + /** + * Paying states whose accounts have a limited number of credits. + * TODO: confirm the header value the API sends for fixed pricing plans. + */ + const LIMITED_CREDIT_PLANS = array( 'free', 'fixed' ); + + const LOW_CREDITS_THRESHOLD = 100; + /** * @var Tiny_Settings settings */ @@ -44,7 +52,7 @@ public function add_dashboard_widget() { self::NAME . '_dashboard_widget', plugins_url( '/css/dashboard-widget.css', __FILE__ ), array(), - self::wp_version(), + self::wp_version() ); wp_add_dashboard_widget( @@ -56,17 +64,26 @@ public function add_dashboard_widget() { public function add_widget_view() { $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - $widget = self::get_widget_data( $optimization_statistics ); + $widget = self::get_widget_data( + $optimization_statistics, + $this->settings->get_remaining_credits(), + $this->settings->get_paying_state(), + $this->settings->has_api_key() + ); + $email_address = $this->settings->get_email_address(); include __DIR__ . '/views/dashboard-widget.php'; } /** * Logic for the view * - * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). + * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). + * @param int|false $remaining_credits Stored account credits, false when unknown. + * @param string|false $paying_state Stored account paying state, false when unknown. + * @param bool $has_api_key * @return array */ - public static function get_widget_data( $optimization_stats ) { + public static function get_widget_data( $optimization_stats, $remaining_credits = false, $paying_state = false, $has_api_key = true ) { $images_remaining = count( $optimization_stats['available-for-optimization'] ); $bytes_total = $optimization_stats['unoptimized-library-size']; $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); @@ -81,20 +98,29 @@ public static function get_widget_data( $optimization_stats ) { : 0; $percentage = max( 0, min( 100, $percentage ) ); + $label = self::get_label_text($percentage); + if ( 0 === $images_total ) { - $status = 'empty'; - $label = ''; + $status = 'empty'; $panda = 'panda-waiting.png'; } elseif ( 0 === $images_remaining ) { $status = 'done'; - $label = __( 'all done', 'tiny-compress-images' ); $panda = 'panda-laying.png'; } else { $status = 'in_progress'; - $label = $percentage >= 50 - ? __( 'almost there', 'tiny-compress-images' ) - : __( 'keep going', 'tiny-compress-images' ); - $panda = 'panda-eating.png'; + $panda = 'panda-waiting.png'; + } + + $has_limited_credits = in_array( $paying_state, self::LIMITED_CREDIT_PLANS, true ) + && is_numeric( $remaining_credits ); + $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; + + if ( ! $has_api_key ) { + $notice = 'no_api_key'; + } elseif ( null !== $remaining_credits && $remaining_credits < self::LOW_CREDITS_THRESHOLD ) { + $notice = 'low_credits'; + } else { + $notice = null; } return array( @@ -106,6 +132,27 @@ public static function get_widget_data( $optimization_stats ) { 'percentage' => $percentage, 'label' => $label, 'panda' => $panda, + 'remaining_credits' => $remaining_credits, + 'notice' => $notice, ); } + + private static function get_label_text($percentage) { + if ($percentage > 99) { + return __( 'optimized', 'tiny-compress-images' ); + } + if ($percentage > 75) { + return __( 'amost there', 'tiny-compress-images' ); + } + + if ($percentage > 50) { + return __( 'halfway there', 'tiny-compress-images' ); + } + + if ($percentage === 0 ) { + return __( '', 'tiny-compress-images' ); + } + + return __( 'getting started', 'tiny-compress-images' ); + } } diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index 342f9d8e..dbc0b355 100644 --- a/src/class-tiny-settings.php +++ b/src/class-tiny-settings.php @@ -219,6 +219,10 @@ protected function get_api_key() { } } + public function has_api_key() { + return (bool) $this->get_api_key(); + } + protected function get_api_key_pending() { if ( defined( 'TINY_API_KEY' ) ) { return false; diff --git a/src/css/dashboard-widget.css b/src/css/dashboard-widget.css index 7e0d3ae8..1aa76303 100644 --- a/src/css/dashboard-widget.css +++ b/src/css/dashboard-widget.css @@ -1,25 +1,68 @@ #tinypng_dashboard_widget .inside { margin: 0; padding: 0; + container-type: inline-size; } .tiny-widget { + display: flex; + min-height: 210px; + background: url("../images/grass.png") no-repeat right bottom / 398px 18px; color: var(--tiny-color-text-muted); font-size: 13px; - line-height: 20px; + line-height: 17px; } -.tiny-widget-bulk { +.tiny-widget-main { display: flex; + flex-shrink: 0; flex-direction: column; + justify-content: space-between; +} + +.tiny-widget-content { + display: flex; + flex: 1; align-items: center; - gap: 5px; - padding: 20px; - text-align: center; + gap: 20px; + padding: 20px 0 20px 20px; +} + +.tiny-widget-stats { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; +} + +.tiny-widget-summary { + display: flex; + flex-direction: column; + gap: 4px; +} + +.tiny-widget-title { + color: #030712; + font-size: 16px; + white-space: nowrap; } -.tiny-widget-bulk strong { - color: #1e1e1e; +.tiny-widget-meta { + display: flex; + gap: 10px; + white-space: nowrap; + color: #989898; + font-size: 11px; +} + +.tiny-widget-meta span { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.tiny-widget-meta svg { + opacity: 0.5; } a.tiny-widget-bulk-button { @@ -27,11 +70,11 @@ a.tiny-widget-bulk-button { align-items: center; justify-content: center; gap: 10px; - width: 209px; - padding: 15px 0; + width: 159px; + height: 37px; background: #3858e9; color: #fff; - font-size: 15px; + font-size: 13px; font-weight: bold; line-height: normal; text-decoration: none; @@ -43,50 +86,12 @@ a.tiny-widget-bulk-button:focus { color: #fff; } -.tiny-widget-body { - display: flex; - align-items: flex-end; - justify-content: space-between; -} - -.tiny-widget-main { - display: flex; - flex-direction: column; - align-self: stretch; - justify-content: space-between; - min-width: 0; -} - -.tiny-widget-progress { - display: flex; - align-items: center; - gap: 20px; -} - -.tiny-widget-progress, -.tiny-widget-main > .tiny-widget-stats { - padding: 20px; -} - -.tiny-widget-stats { - display: flex; - flex-direction: column; - gap: 5px; - line-height: 17px; -} - -.tiny-widget-title { - color: #030712; - font-size: 16px; -} - .tiny-widget-nav { display: flex; align-items: center; padding: 10px 20px; font-size: 12px; font-weight: bold; - line-height: 17px; } .tiny-widget-nav a { @@ -100,19 +105,49 @@ a.tiny-widget-bulk-button:focus { border-left: 1px solid #ccc; } +.tiny-widget-aside { + display: flex; + flex: 1; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + gap: 10px; + min-width: 0; + padding: 20px 12px 0 20px; +} + +a.tiny-widget-notice { + display: block; + padding: 10px; + border-radius: 4px; + background: #fef3e8; + color: #40444f; + font-size: 11px; + text-decoration: none; + text-align: center; +} + +a.tiny-widget-notice:hover, +a.tiny-widget-notice:focus { + background: #fde6cf; + color: #40444f; +} + +.tiny-widget-notice-api-key { + text-align: center; +} + .tiny-widget-panda { - flex-shrink: 0; - width: 213px; + width: 179px; height: auto; - padding: 0 12px; } -.tiny-widget-panda-eating { - transform: scaleX(-1); -} +@container (max-width: 560px) { + .tiny-widget { + background: none; + } -@media (max-width: 480px) { - .tiny-widget-panda { + .tiny-widget-aside { display: none; } } diff --git a/src/views/dashboard-widget.php b/src/views/dashboard-widget.php index 43cfc7b4..e2b6e59b 100644 --- a/src/views/dashboard-widget.php +++ b/src/views/dashboard-widget.php @@ -3,7 +3,8 @@ /** * Dashboard Widget * - * @var array $widget See Tiny_Dashboard::get_widget_data(). + * @var array $widget See Tiny_Dashboard::get_widget_data(). + * @var string $email_address * */ $percentage = $widget['percentage']; @@ -11,72 +12,105 @@ $bulk_url = admin_url('upload.php?page=tiny-bulk-optimization'); $settings_url = admin_url('options-general.php?page=tinify'); +$upgrade_url = 'https://tinypng.com/dashboard/api?type=upgrade&mail=' . + str_replace('%20', '%2B', rawurlencode($email_address)) . + '&utm_source=wordpress-plugin&utm_medium=referral&utm_campaign=upgrade&utm_content=dashboard-widget'; +$strong = array('strong' => array()); +$database_icon = ''; ?>
- -
- - - - - - - %s images.', 'tiny-compress-images'), array('strong' => array())), - esc_html(number_format_i18n($widget['images_remaining'])) - ); - ?> - -
- - -
-
+
+
-
+
-
- -
- - - - + +
+
+ All images are optimized', 'tiny-compress-images'), $strong); + } else { + printf( + /* translators: %s: number of images that can still be optimized */ + wp_kses(_n('%s image needs optimization', '%s images need optimization', $widget['images_remaining'], 'tiny-compress-images'), $strong), + esc_html(number_format_i18n($widget['images_remaining'])) + ); + } ?> +
+ + %s saved', 'tiny-compress-images'), $strong), + esc_html(size_format($widget['bytes_saved'], 1)) + ); + ?> + + + + %s credit', '%s credits remaining', $widget['remaining_credits'], 'tiny-compress-images'), $strong), + esc_html(number_format_i18n($widget['remaining_credits'])) + ); + ?> + + +
+ + + + + +
- -
+ +
+ + diff --git a/src/views/progress-circle.php b/src/views/progress-circle.php index 687d3b14..b62d5f56 100644 --- a/src/views/progress-circle.php +++ b/src/views/progress-circle.php @@ -9,8 +9,8 @@
- %s credits remaining', $widget['remaining_credits'], 'tiny-compress-images'), $strong), esc_html(number_format_i18n($widget['remaining_credits'])) ); @@ -98,10 +98,7 @@ 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' - ), + __( 'You are on a free plan with %s compressions left.', 'tiny-compress-images' ), $strong ), intval( $widget['remaining_credits'] ) diff --git a/test/unit/Tiny_Dashboard_Test.php b/test/unit/Tiny_Dashboard_Test.php index af11be0c..f5715a1e 100644 --- a/test/unit/Tiny_Dashboard_Test.php +++ b/test/unit/Tiny_Dashboard_Test.php @@ -106,11 +106,6 @@ public function test_shows_remaining_credits_on_free_plan() $this->assertSame(210, $this->account_data('210', 'free')['remaining_credits']); } - public function test_shows_remaining_credits_on_fixed_plan() - { - $this->assertSame(210, $this->account_data(210, 'fixed')['remaining_credits']); - } - public function test_hides_remaining_credits_on_paid_plan() { $this->assertNull($this->account_data(210, 'paid')['remaining_credits']); @@ -130,7 +125,7 @@ public function test_has_no_notice_with_enough_credits() public function test_notifies_low_credits_below_100() { $this->assertSame('low_credits', $this->account_data(99, 'free')['notice']); - $this->assertSame('low_credits', $this->account_data(0, 'fixed')['notice']); + $this->assertSame('low_credits', $this->account_data(0, 'free')['notice']); } public function test_does_not_notify_low_credits_on_paid_plan() From 4bf80a7009312d5546e4c2795f5d986fb7d409a1 Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 23 Sep 2026 15:46:17 +0200 Subject: [PATCH 07/18] resolve e2e tests --- test/integration/dashboardwidget.spec.ts | 64 +++++++++++++++++------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/test/integration/dashboardwidget.spec.ts b/test/integration/dashboardwidget.spec.ts index 50836014..5bf0df52 100644 --- a/test/integration/dashboardwidget.spec.ts +++ b/test/integration/dashboardwidget.spec.ts @@ -5,10 +5,23 @@ test.describe.configure({ mode: 'serial' }); let page: Page; +const widget = () => page.locator('#tinypng_dashboard_widget'); + +// The settings page fetches the account details over ajax after saving, +// so wait for that before the dashboard reads them. +async function setAccount(key: string) { + await setAPIKey(page, key); + await page.waitForURL(/settings-updated=true/); + await page.waitForLoadState('networkidle'); +} + test.describe('dashboardwidget', () => { test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); + // The widget hides its notices when narrower than 560px, which it is + // in the two column dashboard at the default 1280px viewport. + page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); await setAPIKey(page, ''); + await setCompressionTiming(page, 'auto'); await enableCompressionSizes(page, [], true); // enable all sizes }); @@ -17,46 +30,59 @@ test.describe('dashboardwidget', () => { }); test('show widget without images', async () => { + await setAPIKey(page, 'JPG123'); + await page.goto('/wp-admin/index.php'); - await expect(page.getByText('You do not seem to have uploaded any JPEG, PNG or WebP images yet.')).toBeVisible(); + + await expect(widget().getByText('George is hungry')).toBeVisible(); + await expect(widget().getByText('There are no images uploaded yet.')).toBeVisible(); }); - test('show widget without optimized images', async () => { - // It won't compress images without an API Key + test('show notice without api key', async () => { await setAPIKey(page, ''); - await setCompressionTiming(page, 'auto'); - - await uploadMedia(page, 'input-example.png'); await page.goto('/wp-admin/index.php'); - await expect( - page.getByText(`Hi Admin, you haven't compressed any images in your media library. If you like you can to optimize your whole library in one go with the bulk optimization page.`) - ).toBeVisible(); + + await expect(widget().getByText('Please register or provide an API key to start compressing images.')).toBeVisible(); }); - test('show widget with some images optimized', async () => { + test('show widget with images to optimize', async () => { + // It won't compress images without an API Key await setAPIKey(page, ''); + await uploadMedia(page, 'input-example.png'); - await setCompressionTiming(page, 'auto'); - - await uploadMedia(page, 'input-example.jpg'); + await page.goto('/wp-admin/index.php'); - await setAPIKey(page, 'JPG123'); + await expect(widget().getByText('1 image needs optimization')).toBeVisible(); + await expect(widget().getByText('Please register or provide an API key to start compressing images.')).toBeVisible(); + await expect(widget().getByRole('link', { name: 'Bulk Optimizer' })).toBeVisible(); + }); + test('show widget with all images optimized', async () => { + await setAPIKey(page, 'JPG123'); await uploadMedia(page, 'input-example.jpg'); await page.goto('/wp-admin/index.php'); - await expect(page.getByText('Admin, you are doing good. With your current settings you can still optimize')).toBeVisible(); + await expect(widget().getByText('All images are optimized')).toBeVisible(); + await expect(widget().getByText('100%')).toBeVisible(); + await expect(widget().getByRole('link', { name: 'Bulk Optimizer' })).not.toBeVisible(); }); - test('show widget with all images optimized', async () => { + test('show notice with low credits on free plan', async () => { + await setAccount('INSUFFICIENTCREDITS123'); + await page.goto('/wp-admin/index.php'); - await uploadMedia(page, 'input-example.jpg'); + await expect(widget().getByText('You are on a free plan with 1 compressions left.')).toBeVisible(); + }); + + test('not show credits on paid plan', async () => { + await setAccount('JPG123'); await page.goto('/wp-admin/index.php'); - await expect(page.getByText('Admin, this is great! Your entire library is optimized!')).toBeVisible(); + await expect(widget().getByText('George is hungry')).toBeVisible(); + await expect(widget().getByText('compressions left')).not.toBeVisible(); }); }); From eb3edbc3f53d0ae8926a31cc1f29a82c660de566 Mon Sep 17 00:00:00 2001 From: tijmen Date: Wed, 23 Sep 2026 15:47:28 +0200 Subject: [PATCH 08/18] format --- src/class-tiny-dashboard.php | 52 ++++++++++++++--------------- src/views/dashboard-widget.php | 60 +++++++++++++++++----------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 2dea3022..849782a6 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -58,13 +58,13 @@ public function add_dashboard_widget() { public function add_widget_view() { $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - $widget = self::get_widget_data( + $widget = self::get_widget_data( $optimization_statistics, $this->settings->get_remaining_credits(), $this->settings->get_paying_state(), $this->settings->has_api_key() ); - $email_address = $this->settings->get_email_address(); + $email_address = $this->settings->get_email_address(); include __DIR__ . '/views/dashboard-widget.php'; } @@ -79,36 +79,36 @@ public function add_widget_view() { */ public static function get_widget_data( $optimization_stats, $remaining_credits = false, $paying_state = false, $has_api_key = true ) { $images_remaining = count( $optimization_stats['available-for-optimization'] ); - $bytes_total = $optimization_stats['unoptimized-library-size']; - $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); - + $bytes_total = $optimization_stats['unoptimized-library-size']; + $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); + $images_optimized = $images_total - $images_remaining; $images_optimized = max( 0, intval( $images_optimized ) ); $images_remaining = max( 0, $images_total - $images_optimized ); - $bytes_saved = $bytes_total - $optimization_stats['optimized-library-size']; + $bytes_saved = $bytes_total - $optimization_stats['optimized-library-size']; $percentage = $images_total > 0 ? intval( floor( $images_optimized / $images_total * 100 ) ) : 0; $percentage = max( 0, min( 100, $percentage ) ); - $label = self::get_label_text($percentage); + $label = self::get_label_text( $percentage ); if ( 0 === $images_total ) { - $status = 'empty'; - $panda = 'panda-waiting.png'; + $status = 'empty'; + $panda = 'panda-waiting.png'; } elseif ( 0 === $images_remaining ) { $status = 'done'; - $panda = 'panda-laying.png'; + $panda = 'panda-laying.png'; } else { $status = 'in_progress'; - $panda = 'panda-waiting.png'; + $panda = 'panda-waiting.png'; } // Only free accounts have a limited number of credits, including // free accounts that bought a credit bundle. $has_limited_credits = 'free' === $paying_state && is_numeric( $remaining_credits ); - $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; + $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; if ( ! $has_api_key ) { $notice = 'no_api_key'; @@ -119,32 +119,32 @@ public static function get_widget_data( $optimization_stats, $remaining_credits } return array( - 'status' => $status, - 'images_optimized' => $images_optimized, - 'images_total' => $images_total, - 'images_remaining' => $images_remaining, - 'bytes_saved' => max( 0, intval( $bytes_saved ) ), - 'percentage' => $percentage, - 'label' => $label, - 'panda' => $panda, + 'status' => $status, + 'images_optimized' => $images_optimized, + 'images_total' => $images_total, + 'images_remaining' => $images_remaining, + 'bytes_saved' => max( 0, intval( $bytes_saved ) ), + 'percentage' => $percentage, + 'label' => $label, + 'panda' => $panda, 'remaining_credits' => $remaining_credits, - 'notice' => $notice, + 'notice' => $notice, ); } - private static function get_label_text($percentage) { - if ($percentage > 99) { + private static function get_label_text( $percentage ) { + if ( $percentage > 99 ) { return __( 'optimized', 'tiny-compress-images' ); } - if ($percentage > 75) { + if ( $percentage > 75 ) { return __( 'amost there', 'tiny-compress-images' ); } - if ($percentage > 50) { + if ( $percentage > 50 ) { return __( 'halfway there', 'tiny-compress-images' ); } - if ($percentage === 0 ) { + if ( $percentage === 0 ) { return ''; } diff --git a/src/views/dashboard-widget.php b/src/views/dashboard-widget.php index 425ae5ab..7e0997cc 100644 --- a/src/views/dashboard-widget.php +++ b/src/views/dashboard-widget.php @@ -8,24 +8,24 @@ * */ $percentage = $widget['percentage']; -$label = $widget['label']; +$label = $widget['label']; -$bulk_url = admin_url('upload.php?page=tiny-bulk-optimization'); -$settings_url = admin_url('options-general.php?page=tinify'); -$upgrade_url = 'https://tinypng.com/dashboard/api?type=upgrade&mail=' . - str_replace('%20', '%2B', rawurlencode($email_address)) . +$bulk_url = admin_url( 'upload.php?page=tiny-bulk-optimization' ); +$settings_url = admin_url( 'options-general.php?page=tinify' ); +$upgrade_url = 'https://tinypng.com/dashboard/api?type=upgrade&mail=' . + str_replace( '%20', '%2B', rawurlencode( $email_address ) ) . '&utm_source=wordpress-plugin&utm_medium=referral&utm_campaign=upgrade&utm_content=dashboard-widget'; -$strong = array('strong' => array()); +$strong = array( 'strong' => array() ); $database_icon = ''; ?>
- +
- - + +
@@ -33,13 +33,13 @@
All images are optimized', 'tiny-compress-images'), $strong); + if ( 'done' === $widget['status'] ) { + echo wp_kses( __( 'All images are optimized', 'tiny-compress-images' ), $strong ); } else { printf( /* translators: %s: number of images that can still be optimized */ - wp_kses(_n('%s image needs optimization', '%s images need optimization', $widget['images_remaining'], 'tiny-compress-images'), $strong), - esc_html(number_format_i18n($widget['images_remaining'])) + wp_kses( _n( '%s image needs optimization', '%s images need optimization', $widget['images_remaining'], 'tiny-compress-images' ), $strong ), + esc_html( number_format_i18n( $widget['images_remaining'] ) ) ); } ?> @@ -50,28 +50,28 @@ echo $database_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup. printf( /* translators: %s: bytes saved, e.g. 21.7 MB */ - wp_kses(__('%s saved', 'tiny-compress-images'), $strong), - esc_html(size_format($widget['bytes_saved'], 1)) + wp_kses( __( '%s saved', 'tiny-compress-images' ), $strong ), + esc_html( size_format( $widget['bytes_saved'], 1 ) ) ); ?> - + %s credit', '%s credits remaining', $widget['remaining_credits'], 'tiny-compress-images'), $strong), - esc_html(number_format_i18n($widget['remaining_credits'])) + wp_kses( _n( '%s credit', '%s credits remaining', $widget['remaining_credits'], 'tiny-compress-images' ), $strong ), + esc_html( number_format_i18n( $widget['remaining_credits'] ) ) ); ?>
- - - + + + @@ -82,18 +82,18 @@
From 41d65701099023fa92f6cd70f6bea22feb0d0766 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 29 Sep 2026 10:12:28 +0200 Subject: [PATCH 09/18] feat: update texts --- src/class-tiny-dashboard.php | 15 +++++++-------- src/views/dashboard-widget.php | 6 +++--- test/integration/dashboardwidget.spec.ts | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 849782a6..f595b40d 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -78,19 +78,18 @@ public function add_widget_view() { * @return array */ public static function get_widget_data( $optimization_stats, $remaining_credits = false, $paying_state = false, $has_api_key = true ) { - $images_remaining = count( $optimization_stats['available-for-optimization'] ); - $bytes_total = $optimization_stats['unoptimized-library-size']; $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); - + $images_remaining = min( + $images_total, + count( $optimization_stats['available-for-optimization'] ) + ); $images_optimized = $images_total - $images_remaining; - $images_optimized = max( 0, intval( $images_optimized ) ); - $images_remaining = max( 0, $images_total - $images_optimized ); - $bytes_saved = $bytes_total - $optimization_stats['optimized-library-size']; + $bytes_saved = $optimization_stats['unoptimized-library-size'] + - $optimization_stats['optimized-library-size']; $percentage = $images_total > 0 ? intval( floor( $images_optimized / $images_total * 100 ) ) : 0; - $percentage = max( 0, min( 100, $percentage ) ); $label = self::get_label_text( $percentage ); @@ -137,7 +136,7 @@ private static function get_label_text( $percentage ) { return __( 'optimized', 'tiny-compress-images' ); } if ( $percentage > 75 ) { - return __( 'amost there', 'tiny-compress-images' ); + return __( 'almost there', 'tiny-compress-images' ); } if ( $percentage > 50 ) { diff --git a/src/views/dashboard-widget.php b/src/views/dashboard-widget.php index 7e0997cc..f7e2b086 100644 --- a/src/views/dashboard-widget.php +++ b/src/views/dashboard-widget.php @@ -61,7 +61,7 @@ echo $database_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static markup. printf( /* translators: %s: number of compression credits left */ - wp_kses( _n( '%s credit', '%s credits remaining', $widget['remaining_credits'], 'tiny-compress-images' ), $strong ), + wp_kses( _n( '%s credit remaining', '%s credits remaining', $widget['remaining_credits'], 'tiny-compress-images' ), $strong ), esc_html( number_format_i18n( $widget['remaining_credits'] ) ) ); ?> @@ -98,10 +98,10 @@ printf( wp_kses( /* translators: %s: number of remaining credits */ - __( 'You are on a free plan with %s compressions left.', 'tiny-compress-images' ), + _n( 'You are on a free plan with %s compression left.', 'You are on a free plan with %s compressions left.', $widget['remaining_credits'], 'tiny-compress-images' ), $strong ), - intval( $widget['remaining_credits'] ) + esc_html( number_format_i18n( $widget['remaining_credits'] ) ) ); ?> diff --git a/test/integration/dashboardwidget.spec.ts b/test/integration/dashboardwidget.spec.ts index 5bf0df52..8efe1d18 100644 --- a/test/integration/dashboardwidget.spec.ts +++ b/test/integration/dashboardwidget.spec.ts @@ -74,7 +74,7 @@ test.describe('dashboardwidget', () => { await page.goto('/wp-admin/index.php'); - await expect(widget().getByText('You are on a free plan with 1 compressions left.')).toBeVisible(); + await expect(widget().getByText('You are on a free plan with 1 compression left.')).toBeVisible(); }); test('not show credits on paid plan', async () => { From 2210523ebed7a22cf280ced6e6e388fc68cbeb35 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 09:43:02 +0200 Subject: [PATCH 10/18] set guard to only allow dashboard widget for users that can upload --- src/class-tiny-dashboard.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index f595b40d..fcb4ee31 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -35,10 +35,12 @@ public function __construct( $settings ) { } public function admin_init() { - add_action( - 'wp_dashboard_setup', - $this->get_method( 'add_dashboard_widget' ) - ); + if ( current_user_can('upload_files') ) { + add_action( + 'wp_dashboard_setup', + array( $this, 'add_dashboard_widget' ) + ); + } } public function add_dashboard_widget() { From 33fdc861af4393d276d37c08872d5b054e3896b8 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 09:43:40 +0200 Subject: [PATCH 11/18] convert to private funcs --- src/class-tiny-dashboard.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index fcb4ee31..9e6cd2b7 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -43,7 +43,7 @@ public function admin_init() { } } - public function add_dashboard_widget() { + private function add_dashboard_widget() { wp_enqueue_style( self::NAME . '_dashboard_widget', plugins_url( '/css/dashboard-widget.css', __FILE__ ), @@ -54,11 +54,11 @@ public function add_dashboard_widget() { wp_add_dashboard_widget( $this->get_prefixed_name( 'dashboard_widget' ), esc_html__( 'TinyPNG - JPEG, PNG & WebP image compression', 'tiny-compress-images' ), - $this->get_method( 'add_widget_view' ) + array($this, 'add_widget_view' ) ); } - public function add_widget_view() { + private function add_widget_view() { $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); $widget = self::get_widget_data( $optimization_statistics, @@ -79,7 +79,7 @@ public function add_widget_view() { * @param bool $has_api_key * @return array */ - public static function get_widget_data( $optimization_stats, $remaining_credits = false, $paying_state = false, $has_api_key = true ) { + private function get_widget_data( $optimization_stats, $remaining_credits = false, $has_api_key = true ) { $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); $images_remaining = min( $images_total, From 5d3035ae531f92f4c99bc3b7e82cbbe65ce44348 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 09:43:50 +0200 Subject: [PATCH 12/18] use settings get pay state --- src/class-tiny-dashboard.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 9e6cd2b7..4782ce07 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -60,10 +60,9 @@ private function add_dashboard_widget() { private function add_widget_view() { $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - $widget = self::get_widget_data( + $widget = $this->get_widget_data( $optimization_statistics, $this->settings->get_remaining_credits(), - $this->settings->get_paying_state(), $this->settings->has_api_key() ); $email_address = $this->settings->get_email_address(); @@ -108,7 +107,7 @@ private function get_widget_data( $optimization_stats, $remaining_credits = fals // Only free accounts have a limited number of credits, including // free accounts that bought a credit bundle. - $has_limited_credits = 'free' === $paying_state && is_numeric( $remaining_credits ); + $has_limited_credits = $this->settings->is_on_free_plan() && is_numeric( $remaining_credits ); $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; if ( ! $has_api_key ) { From 3a4846a9229b786e1290b969afcb97d16ce41168 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 09:44:44 +0200 Subject: [PATCH 13/18] remove unused action and method --- src/class-tiny-plugin.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 0fc7ef7c..c0751435 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -105,11 +105,6 @@ public function ajax_init() { $this->get_method( 'compress_image_for_bulk' ) ); - add_action( - 'wp_ajax_tiny_get_optimization_statistics', - $this->get_method( 'ajax_optimization_statistics' ) - ); - add_action( 'wp_ajax_tiny_get_compression_status', $this->get_method( 'ajax_compression_status' ) @@ -664,15 +659,6 @@ public function compress_image_for_bulk() { exit(); } - public function ajax_optimization_statistics() { - if ( check_ajax_referer( 'tiny-compress', '_nonce', false ) && - current_user_can( 'upload_files' ) ) { - $stats = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - echo json_encode( $stats ); - } - exit(); - } - public function ajax_compression_status() { $response = $this->validate_ajax_attachment_request(); From c63df81324082db33a1cb416184acf52f874d3cf Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 09:48:19 +0200 Subject: [PATCH 14/18] remove default valeus --- src/class-tiny-dashboard.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 4782ce07..e3d51717 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -74,11 +74,10 @@ private function add_widget_view() { * * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). * @param int|false $remaining_credits Stored account credits, false when unknown. - * @param string|false $paying_state Stored account paying state, false when unknown. * @param bool $has_api_key * @return array */ - private function get_widget_data( $optimization_stats, $remaining_credits = false, $has_api_key = true ) { + private function get_widget_data( $optimization_stats, $remaining_credits, $has_api_key ) { $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); $images_remaining = min( $images_total, From da29255b1728a1c66a12ece6f515ea75156bb376 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 09:59:25 +0200 Subject: [PATCH 15/18] format and tests --- src/class-tiny-dashboard.php | 10 +- test/unit/Tiny_Dashboard_Test.php | 263 +++++++++++++++++++++++++----- 2 files changed, 230 insertions(+), 43 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index e3d51717..017fb781 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -35,7 +35,7 @@ public function __construct( $settings ) { } public function admin_init() { - if ( current_user_can('upload_files') ) { + if ( current_user_can( 'upload_files' ) ) { add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) @@ -43,7 +43,7 @@ public function admin_init() { } } - private function add_dashboard_widget() { + public function add_dashboard_widget() { wp_enqueue_style( self::NAME . '_dashboard_widget', plugins_url( '/css/dashboard-widget.css', __FILE__ ), @@ -54,11 +54,11 @@ private function add_dashboard_widget() { wp_add_dashboard_widget( $this->get_prefixed_name( 'dashboard_widget' ), esc_html__( 'TinyPNG - JPEG, PNG & WebP image compression', 'tiny-compress-images' ), - array($this, 'add_widget_view' ) + array( $this, 'add_widget_view' ) ); } - private function add_widget_view() { + public function add_widget_view() { $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); $widget = $this->get_widget_data( $optimization_statistics, @@ -77,7 +77,7 @@ private function add_widget_view() { * @param bool $has_api_key * @return array */ - private function get_widget_data( $optimization_stats, $remaining_credits, $has_api_key ) { + public function get_widget_data( $optimization_stats, $remaining_credits, $has_api_key ) { $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); $images_remaining = min( $images_total, diff --git a/test/unit/Tiny_Dashboard_Test.php b/test/unit/Tiny_Dashboard_Test.php index f5715a1e..1741f9ff 100644 --- a/test/unit/Tiny_Dashboard_Test.php +++ b/test/unit/Tiny_Dashboard_Test.php @@ -5,11 +5,28 @@ class Tiny_Dashboard_Test extends Tiny_TestCase { /** - * Builds the widget data + * @var Tiny_Settings|\PHPUnit\Framework\MockObject\MockObject */ - private function widget_data($uploaded, $remaining, $unoptimized_size = 0, $optimized_size = 0) + private $settings; + + public function set_up() { - return Tiny_Dashboard::get_widget_data(array( + parent::set_up(); + $this->settings = $this->createMock(Tiny_Settings::class); + } + + /** + * helper to build optimization stats + * + * @param int $uploaded number of uploaded images + * @param int $remaining number of images remaining + * @param integer $unoptimized_size + * @param integer $optimized_size + * @return void + */ + private function stats($uploaded, $remaining, $unoptimized_size = 0, $optimized_size = 0) + { + return array( 'uploaded-images' => $uploaded, 'optimized-image-sizes' => 0, 'available-unoptimized-sizes' => 0, @@ -18,12 +35,19 @@ private function widget_data($uploaded, $remaining, $unoptimized_size = 0, $opti 'estimated_credit_use' => 0, 'available-for-optimization' => array_fill(0, $remaining, array('ID' => 1, 'post_title' => 'image')), 'display-percentage' => 0, - )); + ); } public function test_is_empty_when_there_are_no_images() { - $data = $this->widget_data(0, 0); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(0, 0), + 0, + false + ); $this->assertSame('empty', $data['status']); $this->assertSame(0, $data['percentage']); @@ -33,7 +57,14 @@ public function test_is_empty_when_there_are_no_images() public function test_is_in_progress_when_images_remain() { - $data = $this->widget_data(1250, 330); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(1250, 330), + 0, + false + ); $this->assertSame('in_progress', $data['status']); $this->assertSame(920, $data['images_optimized']); @@ -45,7 +76,14 @@ public function test_is_in_progress_when_images_remain() public function test_is_done_when_no_images_remain() { - $data = $this->widget_data(1250, 0); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(1250, 0), + 0, + false + ); $this->assertSame('done', $data['status']); $this->assertSame(1250, $data['images_optimized']); @@ -54,24 +92,93 @@ public function test_is_done_when_no_images_remain() $this->assertSame('panda-laying.png', $data['panda']); } - public function test_labels_below_half_as_keep_going() + public function test_labels_up_to_half_as_getting_started() { - $this->assertSame('getting started', $this->widget_data(100, 51)['label']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + + $one_percent = $dashboard->get_widget_data( + $this->stats(100, 99), + 0, + false + ); + $fifty_percent = $dashboard->get_widget_data( + $this->stats(100, 50), + 0, + false + ); + + $this->assertSame('getting started', $one_percent['label']); + $this->assertSame('getting started', $fifty_percent['label']); } - public function test_labels_half_and_above_as_almost_there() + public function test_labels_above_half_as_halfway_there() { - $this->assertSame('getting started', $this->widget_data(100, 50)['label']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + + $fifty_one_percent = $dashboard->get_widget_data( + $this->stats(100, 49), + 0, + false + ); + $seventy_five_percent = $dashboard->get_widget_data( + $this->stats(100, 25), + 0, + false + ); + + $this->assertSame('halfway there', $fifty_one_percent['label']); + $this->assertSame('halfway there', $seventy_five_percent['label']); + } + + public function test_labels_above_three_quarters_as_almost_there() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + + $seventy_six_percent = $dashboard->get_widget_data( + $this->stats(100, 24), + 0, + false + ); + $ninety_nine_percent = $dashboard->get_widget_data( + $this->stats(1000, 1), + 0, + false + ); + + $this->assertSame('almost there', $seventy_six_percent['label']); + $this->assertSame('almost there', $ninety_nine_percent['label']); } public function test_has_no_label_when_nothing_is_optimized() { - $this->assertSame('', $this->widget_data(10, 10)['label']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 10), + 0, + false + ); + + $this->assertSame('', $data['label']); } public function test_does_not_show_100_percent_while_images_remain() { - $data = $this->widget_data(1000, 1); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(1000, 1), + 0, + false + ); $this->assertSame('in_progress', $data['status']); $this->assertSame(99, $data['percentage']); @@ -79,67 +186,147 @@ public function test_does_not_show_100_percent_while_images_remain() public function test_bytes_saved_is_the_library_size_reduction() { - $this->assertSame(2000, $this->widget_data(10, 3, 5000, 3000)['bytes_saved']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 3, 5000, 3000), + 0, + false + ); + + $this->assertSame(2000, $data['bytes_saved']); } public function test_bytes_saved_is_never_negative() { - $this->assertSame(0, $this->widget_data(10, 3, 3000, 5000)['bytes_saved']); - } + $this->settings->method('is_on_free_plan')->willReturn(true); - private function account_data($remaining_credits, $paying_state, $has_api_key = true) - { - return Tiny_Dashboard::get_widget_data(array( - 'uploaded-images' => 10, - 'optimized-image-sizes' => 0, - 'available-unoptimized-sizes' => 0, - 'optimized-library-size' => 0, - 'unoptimized-library-size' => 0, - 'estimated_credit_use' => 0, - 'available-for-optimization' => array(array('ID' => 1, 'post_title' => 'image')), - 'display-percentage' => 0, - ), $remaining_credits, $paying_state, $has_api_key); + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 3, 3000, 5000), + 0, + false + ); + + $this->assertSame(0, $data['bytes_saved']); } public function test_shows_remaining_credits_on_free_plan() { - $this->assertSame(210, $this->account_data('210', 'free')['remaining_credits']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + '210', + true + ); + + $this->assertSame(210, $data['remaining_credits']); } public function test_hides_remaining_credits_on_paid_plan() { - $this->assertNull($this->account_data(210, 'paid')['remaining_credits']); + $this->settings->method('is_on_free_plan')->willReturn(false); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + 210, + true + ); + + $this->assertNull($data['remaining_credits']); } public function test_hides_remaining_credits_when_unknown() { - $this->assertNull($this->account_data(false, 'free')['remaining_credits']); - $this->assertNull($this->account_data(210, false)['remaining_credits']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + false, + true + ); + + $this->assertNull($data['remaining_credits']); } public function test_has_no_notice_with_enough_credits() { - $this->assertNull($this->account_data(100, 'free')['notice']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + 100, + true + ); + + $this->assertNull($data['notice']); } public function test_notifies_low_credits_below_100() { - $this->assertSame('low_credits', $this->account_data(99, 'free')['notice']); - $this->assertSame('low_credits', $this->account_data(0, 'free')['notice']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $just_below = $dashboard->get_widget_data( + $this->stats(10, 1), + 99, + true + ); + $none_left = $dashboard->get_widget_data( + $this->stats(10, 1), + 0, + true + ); + + $this->assertSame('low_credits', $just_below['notice']); + $this->assertSame('low_credits', $none_left['notice']); } public function test_does_not_notify_low_credits_on_paid_plan() { - $this->assertNull($this->account_data(0, 'paid')['notice']); + $this->settings->method('is_on_free_plan')->willReturn(false); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + 0, + true + ); + + $this->assertNull($data['notice']); } public function test_notifies_missing_api_key() { - $this->assertSame('no_api_key', $this->account_data(false, false, false)['notice']); + $this->settings->method('is_on_free_plan')->willReturn(false); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + false, + false + ); + + $this->assertSame('no_api_key', $data['notice']); } public function test_missing_api_key_takes_precedence_over_low_credits() { - $this->assertSame('no_api_key', $this->account_data(10, 'free', false)['notice']); + $this->settings->method('is_on_free_plan')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data( + $this->stats(10, 1), + 10, + false + ); + + $this->assertSame('no_api_key', $data['notice']); } } From 1b50e802fad736d246948a5395abd7ec0165b324 Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 10:13:10 +0200 Subject: [PATCH 16/18] read credits and api key from settings in widget data --- src/class-tiny-dashboard.php | 15 +-- test/unit/Tiny_Dashboard_Test.php | 184 +++++++++--------------------- 2 files changed, 56 insertions(+), 143 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 017fb781..3480e24b 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -60,11 +60,7 @@ public function add_dashboard_widget() { public function add_widget_view() { $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - $widget = $this->get_widget_data( - $optimization_statistics, - $this->settings->get_remaining_credits(), - $this->settings->has_api_key() - ); + $widget = $this->get_widget_data( $optimization_statistics ); $email_address = $this->settings->get_email_address(); include __DIR__ . '/views/dashboard-widget.php'; } @@ -72,12 +68,10 @@ public function add_widget_view() { /** * Logic for the view * - * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). - * @param int|false $remaining_credits Stored account credits, false when unknown. - * @param bool $has_api_key + * @param array $optimization_stats See Tiny_Bulk_Optimization::get_optimization_statistics(). * @return array */ - public function get_widget_data( $optimization_stats, $remaining_credits, $has_api_key ) { + public function get_widget_data( $optimization_stats ) { $images_total = max( 0, intval( $optimization_stats['uploaded-images'] ) ); $images_remaining = min( $images_total, @@ -106,10 +100,11 @@ public function get_widget_data( $optimization_stats, $remaining_credits, $has_a // Only free accounts have a limited number of credits, including // free accounts that bought a credit bundle. + $remaining_credits = $this->settings->get_remaining_credits(); $has_limited_credits = $this->settings->is_on_free_plan() && is_numeric( $remaining_credits ); $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; - if ( ! $has_api_key ) { + if ( ! $this->settings->has_api_key() ) { $notice = 'no_api_key'; } elseif ( null !== $remaining_credits && $remaining_credits < self::LOW_CREDITS_THRESHOLD ) { $notice = 'low_credits'; diff --git a/test/unit/Tiny_Dashboard_Test.php b/test/unit/Tiny_Dashboard_Test.php index 1741f9ff..a802b6fc 100644 --- a/test/unit/Tiny_Dashboard_Test.php +++ b/test/unit/Tiny_Dashboard_Test.php @@ -22,7 +22,7 @@ public function set_up() * @param int $remaining number of images remaining * @param integer $unoptimized_size * @param integer $optimized_size - * @return void + * @return array optimization stats */ private function stats($uploaded, $remaining, $unoptimized_size = 0, $optimized_size = 0) { @@ -40,14 +40,8 @@ private function stats($uploaded, $remaining, $unoptimized_size = 0, $optimized_ public function test_is_empty_when_there_are_no_images() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(0, 0), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(0, 0)); $this->assertSame('empty', $data['status']); $this->assertSame(0, $data['percentage']); @@ -57,14 +51,8 @@ public function test_is_empty_when_there_are_no_images() public function test_is_in_progress_when_images_remain() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(1250, 330), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(1250, 330)); $this->assertSame('in_progress', $data['status']); $this->assertSame(920, $data['images_optimized']); @@ -76,14 +64,8 @@ public function test_is_in_progress_when_images_remain() public function test_is_done_when_no_images_remain() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(1250, 0), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(1250, 0)); $this->assertSame('done', $data['status']); $this->assertSame(1250, $data['images_optimized']); @@ -94,20 +76,10 @@ public function test_is_done_when_no_images_remain() public function test_labels_up_to_half_as_getting_started() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $one_percent = $dashboard->get_widget_data( - $this->stats(100, 99), - 0, - false - ); - $fifty_percent = $dashboard->get_widget_data( - $this->stats(100, 50), - 0, - false - ); + $one_percent = $dashboard->get_widget_data($this->stats(100, 99)); + $fifty_percent = $dashboard->get_widget_data($this->stats(100, 50)); $this->assertSame('getting started', $one_percent['label']); $this->assertSame('getting started', $fifty_percent['label']); @@ -115,20 +87,10 @@ public function test_labels_up_to_half_as_getting_started() public function test_labels_above_half_as_halfway_there() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $fifty_one_percent = $dashboard->get_widget_data( - $this->stats(100, 49), - 0, - false - ); - $seventy_five_percent = $dashboard->get_widget_data( - $this->stats(100, 25), - 0, - false - ); + $fifty_one_percent = $dashboard->get_widget_data($this->stats(100, 49)); + $seventy_five_percent = $dashboard->get_widget_data($this->stats(100, 25)); $this->assertSame('halfway there', $fifty_one_percent['label']); $this->assertSame('halfway there', $seventy_five_percent['label']); @@ -136,20 +98,10 @@ public function test_labels_above_half_as_halfway_there() public function test_labels_above_three_quarters_as_almost_there() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $seventy_six_percent = $dashboard->get_widget_data( - $this->stats(100, 24), - 0, - false - ); - $ninety_nine_percent = $dashboard->get_widget_data( - $this->stats(1000, 1), - 0, - false - ); + $seventy_six_percent = $dashboard->get_widget_data($this->stats(100, 24)); + $ninety_nine_percent = $dashboard->get_widget_data($this->stats(1000, 1)); $this->assertSame('almost there', $seventy_six_percent['label']); $this->assertSame('almost there', $ninety_nine_percent['label']); @@ -157,28 +109,16 @@ public function test_labels_above_three_quarters_as_almost_there() public function test_has_no_label_when_nothing_is_optimized() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 10), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(10, 10)); $this->assertSame('', $data['label']); } public function test_does_not_show_100_percent_while_images_remain() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(1000, 1), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(1000, 1)); $this->assertSame('in_progress', $data['status']); $this->assertSame(99, $data['percentage']); @@ -186,28 +126,16 @@ public function test_does_not_show_100_percent_while_images_remain() public function test_bytes_saved_is_the_library_size_reduction() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 3, 5000, 3000), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(10, 3, 5000, 3000)); $this->assertSame(2000, $data['bytes_saved']); } public function test_bytes_saved_is_never_negative() { - $this->settings->method('is_on_free_plan')->willReturn(true); - $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 3, 3000, 5000), - 0, - false - ); + $data = $dashboard->get_widget_data($this->stats(10, 3, 3000, 5000)); $this->assertSame(0, $data['bytes_saved']); } @@ -215,13 +143,11 @@ public function test_bytes_saved_is_never_negative() public function test_shows_remaining_credits_on_free_plan() { $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn('210'); + $this->settings->method('has_api_key')->willReturn(true); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - '210', - true - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertSame(210, $data['remaining_credits']); } @@ -229,13 +155,11 @@ public function test_shows_remaining_credits_on_free_plan() public function test_hides_remaining_credits_on_paid_plan() { $this->settings->method('is_on_free_plan')->willReturn(false); + $this->settings->method('get_remaining_credits')->willReturn(210); + $this->settings->method('has_api_key')->willReturn(true); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - 210, - true - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertNull($data['remaining_credits']); } @@ -243,13 +167,11 @@ public function test_hides_remaining_credits_on_paid_plan() public function test_hides_remaining_credits_when_unknown() { $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(false); + $this->settings->method('has_api_key')->willReturn(true); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - false, - true - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertNull($data['remaining_credits']); } @@ -257,13 +179,11 @@ public function test_hides_remaining_credits_when_unknown() public function test_has_no_notice_with_enough_credits() { $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(100); + $this->settings->method('has_api_key')->willReturn(true); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - 100, - true - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertNull($data['notice']); } @@ -271,33 +191,35 @@ public function test_has_no_notice_with_enough_credits() public function test_notifies_low_credits_below_100() { $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(99); + $this->settings->method('has_api_key')->willReturn(true); $dashboard = new Tiny_Dashboard($this->settings); - $just_below = $dashboard->get_widget_data( - $this->stats(10, 1), - 99, - true - ); - $none_left = $dashboard->get_widget_data( - $this->stats(10, 1), - 0, - true - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); + + $this->assertSame('low_credits', $data['notice']); + } + + public function test_notifies_low_credits_when_none_are_left() + { + $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(0); + $this->settings->method('has_api_key')->willReturn(true); + + $dashboard = new Tiny_Dashboard($this->settings); + $data = $dashboard->get_widget_data($this->stats(10, 1)); - $this->assertSame('low_credits', $just_below['notice']); - $this->assertSame('low_credits', $none_left['notice']); + $this->assertSame('low_credits', $data['notice']); } public function test_does_not_notify_low_credits_on_paid_plan() { $this->settings->method('is_on_free_plan')->willReturn(false); + $this->settings->method('get_remaining_credits')->willReturn(0); + $this->settings->method('has_api_key')->willReturn(true); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - 0, - true - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertNull($data['notice']); } @@ -305,13 +227,11 @@ public function test_does_not_notify_low_credits_on_paid_plan() public function test_notifies_missing_api_key() { $this->settings->method('is_on_free_plan')->willReturn(false); + $this->settings->method('get_remaining_credits')->willReturn(false); + $this->settings->method('has_api_key')->willReturn(false); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - false, - false - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertSame('no_api_key', $data['notice']); } @@ -319,13 +239,11 @@ public function test_notifies_missing_api_key() public function test_missing_api_key_takes_precedence_over_low_credits() { $this->settings->method('is_on_free_plan')->willReturn(true); + $this->settings->method('get_remaining_credits')->willReturn(10); + $this->settings->method('has_api_key')->willReturn(false); $dashboard = new Tiny_Dashboard($this->settings); - $data = $dashboard->get_widget_data( - $this->stats(10, 1), - 10, - false - ); + $data = $dashboard->get_widget_data($this->stats(10, 1)); $this->assertSame('no_api_key', $data['notice']); } From da613b8ad1c893a264441207c5f3a0b258fb50fc Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 10:26:50 +0200 Subject: [PATCH 17/18] formatting --- src/class-tiny-dashboard.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 3480e24b..6d4f1ee9 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -59,9 +59,9 @@ public function add_dashboard_widget() { } public function add_widget_view() { - $optimization_statistics = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); - $widget = $this->get_widget_data( $optimization_statistics ); - $email_address = $this->settings->get_email_address(); + $stats = Tiny_Bulk_Optimization::get_optimization_statistics( $this->settings ); + $widget = $this->get_widget_data( $stats ); + $email_address = $this->settings->get_email_address(); include __DIR__ . '/views/dashboard-widget.php'; } @@ -101,12 +101,14 @@ public function get_widget_data( $optimization_stats ) { // Only free accounts have a limited number of credits, including // free accounts that bought a credit bundle. $remaining_credits = $this->settings->get_remaining_credits(); - $has_limited_credits = $this->settings->is_on_free_plan() && is_numeric( $remaining_credits ); + $has_limited_credits = $this->settings->is_on_free_plan() && + is_numeric( $remaining_credits ); $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; if ( ! $this->settings->has_api_key() ) { $notice = 'no_api_key'; - } elseif ( null !== $remaining_credits && $remaining_credits < self::LOW_CREDITS_THRESHOLD ) { + } elseif ( null !== $remaining_credits && + $remaining_credits < self::LOW_CREDITS_THRESHOLD ) { $notice = 'low_credits'; } else { $notice = null; @@ -138,7 +140,7 @@ private static function get_label_text( $percentage ) { return __( 'halfway there', 'tiny-compress-images' ); } - if ( $percentage === 0 ) { + if ( 0 === $percentage ) { return ''; } From a58f8a46c0db5655ea4c920a5617838f92b8354b Mon Sep 17 00:00:00 2001 From: tijmen Date: Tue, 6 Oct 2026 10:30:23 +0200 Subject: [PATCH 18/18] add doc --- src/class-tiny-dashboard.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 6d4f1ee9..7a06f5bd 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -128,6 +128,12 @@ public function get_widget_data( $optimization_stats ) { ); } + /** + * Returns a description based on the percentage of optimized library + * + * @param int $percentage + * @return string label + */ private static function get_label_text( $percentage ) { if ( $percentage > 99 ) { return __( 'optimized', 'tiny-compress-images' );