Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/admin/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ public function register_settings() {
],

// Image Optimization settings
'smart_optimization' => [
'type' => 'integer',
],
'webp_quality' => [
'type' => 'integer',
],
Expand Down Expand Up @@ -322,6 +325,11 @@ public function sanitize_options( $options ) {
$sanitized['thumbnail_sizes'] = array_map( 'sanitize_text_field', $options['thumbnail_sizes'] );
}

// Sanitize smart optimization
if ( isset( $options['smart_optimization'] ) ) {
$sanitized['smart_optimization'] = $options['smart_optimization'] ? 1 : 0;
}

// Sanitize webp quality
if ( isset( $options['webp_quality'] ) ) {
$quality = absint( $options['webp_quality'] );
Expand Down
19 changes: 14 additions & 5 deletions src/admin/class-meta-box.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,20 @@ function cimo_get_media_type_label( $mimetype ) {

// Converted format
echo '<li class="cimo-converted">';
echo '🏞️ ' . sprintf(
/* translators: %s: converted format */
esc_html__( 'Converted to %s', 'cimo-image-optimizer' ),
'<span class="cimo-value">' . esc_html( $converted_format ) . '</span>'
);
$converted_format_markup = '<span class="cimo-value">' . esc_html( $converted_format ) . '</span>';
if ( ! empty( $cimo['smartOptimized'] ) ) {
echo '🏞️ ' . sprintf(
/* translators: %s: converted format */
esc_html__( 'Smart optimized to %s', 'cimo-image-optimizer' ),
$converted_format_markup
);
} else {
echo '🏞️ ' . sprintf(
/* translators: %s: converted format */
esc_html__( 'Converted to %s', 'cimo-image-optimizer' ),
$converted_format_markup
);
}
echo '</li>';

// Conversion time
Expand Down
7 changes: 7 additions & 0 deletions src/admin/class-metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function register_rest_route() {
'convertedFilesize',
'conversionTime',
'compressionSavings',
'smartOptimized',
];
if ( ! is_array( $value ) ) {
// translators: The %s is the parameter name.
Expand Down Expand Up @@ -85,6 +86,7 @@ public function register_rest_route() {
'convertedFilesize',
'conversionTime',
'compressionSavings',
'smartOptimized',
];
$sanitized = [];
if ( is_array( $value ) ) {
Expand All @@ -98,6 +100,8 @@ public function register_rest_route() {
$entry[ $key ] = intval( $item[ $key ] );
} elseif ( in_array( $key, [ 'conversionTime', 'compressionSavings' ], true ) ) {
$entry[ $key ] = floatval( $item[ $key ] );
} elseif ( $key === 'smartOptimized' ) {
$entry[ $key ] = ! empty( $item[ $key ] ) ? 1 : 0;
} else {
$entry[ $key ] = sanitize_text_field( $item[ $key ] );
}
Expand Down Expand Up @@ -137,6 +141,7 @@ public function save_metadata( $request ) {
'convertedFilesize',
'conversionTime',
'compressionSavings',
'smartOptimized',
];
$sanitized_metadata = [];
foreach ( $metadata_array as $item ) {
Expand All @@ -149,6 +154,8 @@ public function save_metadata( $request ) {
$entry[ $key ] = intval( $item[ $key ] );
} elseif ( in_array( $key, [ 'conversionTime', 'compressionSavings' ], true ) ) {
$entry[ $key ] = floatval( $item[ $key ] );
} elseif ( $key === 'smartOptimized' ) {
$entry[ $key ] = ! empty( $item[ $key ] ) ? 1 : 0;
} else {
$entry[ $key ] = sanitize_text_field( $item[ $key ] );
}
Expand Down
3 changes: 3 additions & 0 deletions src/admin/class-script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public static function enqueue_cimo_assets() {
'canManageOptions' => current_user_can( 'manage_options' ),
'optimizeAllMedia' => isset( $settings['optimize_all_media'] ) ? (int) $settings['optimize_all_media'] : 0,
'isPremium' => CIMO_BUILD === 'premium',
'smartOptimization' => CIMO_BUILD === 'premium'
? ( isset( $settings['smart_optimization'] ) ? (int) $settings['smart_optimization'] : 1 )
: 0,
'webpQuality' => ! empty( $settings['webp_quality'] ) ? (int) $settings['webp_quality'] : 80,
'maxImageDimension' => ! empty( $settings['max_image_dimension'] ) ? (int) $settings['max_image_dimension'] : 0,
'videoOptimizationEnabled' => isset( $settings['video_optimization_enabled'] ) ? (int) $settings['video_optimization_enabled'] : 1,
Expand Down
20 changes: 20 additions & 0 deletions src/admin/css/admin-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,26 @@
order: 1;
}

.cimo-smart-optimization-toggle {
order: 2;

~ .cimo-setting-field {
order: 3;
}

~ .cimo-webp-quality-range-control {
order: 1;
}

~ .cimo-reset-button {
order: 10;
}
}

.cimo-is-premium .cimo-smart-optimization-toggle {
order: 1;
}

@keyframes cimo-bulk-optimizer-blink {

0%, 100% { opacity: 1; }
Expand Down
2 changes: 1 addition & 1 deletion src/admin/js/media-manager/drop-zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function addDropZoneListenerToMediaManager( targetDocument ) {
const optimizedResults = await Promise.all(
fileConverters.map( async converter => {
try {
const result = await converter.convert()
const result = await converter.optimize()
if ( result.error ) {
// eslint-disable-next-line no-console
console.warn( result.error )
Expand Down
32 changes: 26 additions & 6 deletions src/admin/js/media-manager/progress-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,34 @@ class ProgressModal {
this.interval = null
this.modal = null
this.progressBars = []
this._setupModal()
this.delayTimeout = null
}

open() {
if ( ! this.modal ) {
return
}
if ( this.converters.length === 0 ) {
return
}
this.modal.style.display = 'flex'
this._startInterval()

const delays = this.converters
.map( c => c?.progressDelay )
.filter( v => typeof v === 'number' && v > 0 )

// Only delay if all converter opt to delay, to allow
// big media files like video to show the progress modal immediately.
const shouldDelay = delays.length === this.converters.length
const delay = shouldDelay ? Math.max( ...delays ) : 0

const start = () => {
this._setupModal()
this.modal.style.display = 'flex'
this._startInterval()
}

if ( delay > 0 ) {
this.delayTimeout = setTimeout( start, delay )
} else {
start()
}
}

_handleCloseClick() {
Expand All @@ -37,6 +53,10 @@ class ProgressModal {
}

close() {
if ( this.delayTimeout ) {
clearTimeout( this.delayTimeout )
this.delayTimeout = null
}
if ( ! this.modal ) {
return
}
Expand Down
2 changes: 1 addition & 1 deletion src/admin/js/media-manager/select-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function addSelectFilesListenerToFileUploads( targetDocument ) {
const optimizedResults = await Promise.all(
fileConverters.map( async converter => {
try {
const result = await converter.convert()
const result = await converter.optimize()
if ( result.error ) {
// eslint-disable-next-line no-console
console.warn( result.error )
Expand Down
16 changes: 15 additions & 1 deletion src/admin/js/media-manager/sidebar-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,23 @@ function injectCimoMetadata( {
}

if ( ! isBulkOptimized ) {
const formatLabel = convertMimetypeToFormat( customMetadata.convertedFormat )
const convertedFormatSpan = `<span class="cimo-value">${ escape( formatLabel ) }</span>`
const convertedLineText = customMetadata.smartOptimized
? sprintf(
/* translators: %s: image format name (e.g. WebP) */
__( 'Smart optimized to %s', 'cimo-image-optimizer' ),
convertedFormatSpan
)
: sprintf(
/* translators: %s: image format name (e.g. WebP) */
__( 'Converted to %s', 'cimo-image-optimizer' ),
convertedFormatSpan
)

html += `
<li class="cimo-converted">
🏞️ Converted to <span class="cimo-value">${ escape( convertMimetypeToFormat( customMetadata.convertedFormat ) ) }</span>
🏞️ ${ convertedLineText }
</li>
`

Expand Down
56 changes: 43 additions & 13 deletions src/admin/js/page/admin-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const AdminSettings = () => {
thumbnailSizes: [], // Stores DISABLED thumbnail sizes

// Image optimization settings
smartOptimization: 1,
webpQuality: 80,
maxImageDimension: '',

Expand Down Expand Up @@ -90,6 +91,7 @@ const AdminSettings = () => {
thumbnailSizes: cimoOptions.thumbnail_sizes || [],

// Image Optimization settings
smartOptimization: cimoOptions.smart_optimization !== undefined ? cimoOptions.smart_optimization : 1,
webpQuality: cimoOptions.webp_quality !== undefined ? cimoOptions.webp_quality : 80,
maxImageDimension: cimoOptions.max_image_dimension || '',

Expand Down Expand Up @@ -179,6 +181,7 @@ const AdminSettings = () => {
setSettings( settings => {
return {
...settings,
smartOptimization: 1,
webpQuality: 80,
maxImageDimension: 1920,
}
Expand All @@ -189,6 +192,7 @@ const AdminSettings = () => {
setSettings( settings => {
return {
...settings,
smartOptimization: 1,
webpQuality: '',
maxImageDimension: '',
}
Expand Down Expand Up @@ -295,6 +299,7 @@ const AdminSettings = () => {
thumbnail_sizes: settings.thumbnailSizes,

// Image Optimization settings
smart_optimization: settings.smartOptimization,
webp_quality: parseInt( settings.webpQuality ) || 0,
Comment on lines +302 to 303
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Free mode still allows a non-static image quality.

Line 303 saves whatever webpQuality is in state, and Line 600 still renders an editable slider for free users. Issue #35 says free uploads should stay on a fixed 80% quality, so this either violates the spec or exposes a control the runtime is supposed to ignore. Lock free saves to 80 and make the free UI read-only or hide it.

🔧 Possible fix
 						// Image Optimization settings
 						smart_optimization: settings.smartOptimization,
-						webp_quality: parseInt( settings.webpQuality ) || 0,
+						webp_quality: buildType === 'free' ? 80 : ( parseInt( settings.webpQuality, 10 ) || 0 ),
 						max_image_dimension: parseInt( settings.maxImageDimension ) || 0,
@@
-						{ ( buildType === 'free' || settings.smartOptimization === 0 ) && (
+						{ buildType === 'free' && (
+							<div className="cimo-setting-field cimo-webp-quality-range-control">
+								<RangeControl
+									id="webpQuality"
+									label={ __( 'WebP Image Quality', 'cimo-image-optimizer' ) }
+									value={ 80 }
+									min="1"
+									max="100"
+									step="1"
+									disabled
+									__next40pxDefaultSize
+									help={ __( 'Free builds use a fixed 80% WebP quality.', 'cimo-image-optimizer' ) }
+								/>
+							</div>
+						) }
+						{ buildType !== 'free' && settings.smartOptimization === 0 && (
 							<div className="cimo-setting-field cimo-webp-quality-range-control">
 								<RangeControl
 									id="webpQuality"

Also applies to: 600-614

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/js/page/admin-settings.js` around lines 302 - 303, The code
currently writes webp_quality from settings.webpQuality and still renders an
editable slider for non-paid users; change the save logic so that when the
current account is on free tier the saved webp_quality is forced to 80 (e.g.,
replace writing webp_quality: parseInt(settings.webpQuality) || 0 with
conditional logic that sets webp_quality = 80 for free users, otherwise uses
parseInt(settings.webpQuality)), and update the settings UI rendering (the
component that renders the webp quality slider) to be read-only or hidden for
free users so the control is not editable—use the existing user-tier flag/check
in the code to gate both the save in the block that sets
smart_optimization/webp_quality and the slider rendering.

max_image_dimension: parseInt( settings.maxImageDimension ) || 0,

Expand Down Expand Up @@ -570,21 +575,46 @@ const AdminSettings = () => {
</Button>
</div>

<div className="cimo-setting-field">
<RangeControl
id="webpQuality"
label={ __( 'WebP Image Quality', 'cimo-image-optimizer' ) }
value={ settings.webpQuality || '' }
onChange={ value => handleInputChange( 'webpQuality', value || '' ) }
min="1"
max="100"
step="1"
__next40pxDefaultSize
allowReset
initialPosition={ 80 }
help={ __( 'Set the quality / compression level for generated .webp images. Default is 80%. Higher values mean better quality and larger file size; lower values reduce file size with more compression but lower quality.', 'cimo-image-optimizer' ) }
{ /* Smart Optimization */ }
<div className="cimo-setting-field cimo-smart-optimization-toggle">
<ToggleControl
__nextHasNoMarginBottom
label={
<span>
{ __( 'Smart Optimization', 'cimo-image-optimizer' ) }
{ buildType === 'free' && (
<span className="cimo-premium-tag">
{ __( 'Premium', 'cimo-image-optimizer' ) }
</span>
) }
</span>
}
checked={ buildType === 'free' ? false : ( settings.smartOptimization === 1 ) }
disabled={ buildType === 'free' }
onChange={ checked => handleInputChange( 'smartOptimization', checked ? 1 : 0 ) }
help={ __( 'Smart Optimization uses our advanced algorithms to choose the best compression and quality settings per image. This adds a small overhead to the upload process, but the results are even smaller file sizes and faster loading times.', 'cimo-image-optimizer' ) }
/>
</div>

{ /* WebP Image Quality */ }
{ ( buildType === 'free' || settings.smartOptimization === 0 ) && (
<div className="cimo-setting-field cimo-webp-quality-range-control">
<RangeControl
id="webpQuality"
label={ __( 'WebP Image Quality', 'cimo-image-optimizer' ) }
value={ settings.webpQuality || '' }
onChange={ value => handleInputChange( 'webpQuality', value || '' ) }
min="1"
max="100"
step="1"
__next40pxDefaultSize
allowReset
initialPosition={ 80 }
help={ __( 'Set the quality / compression level for generated .webp images. Default is 80%. Higher values mean better quality and larger file size; lower values reduce file size with more compression but lower quality.', 'cimo-image-optimizer' ) }
/>
</div>
) }

{ /* Maximum Image Dimension */ }
<div className="cimo-setting-field">
<TextControl
Expand Down
50 changes: 49 additions & 1 deletion src/shared/converters/converter-abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ class Converter {
return this._progress
}

/**
* Set the current progress value for this converter. Should be a value
* between 0 and 1 inclusive. Consumers can set this to update progress
* indicators.
*
* @param {number} value - Value between 0 and 1 inclusive.
*/
set progress( value ) {
this._progress = Math.min( 1, Math.max( 0, value ) )
}

/**
* Whether to show a progress indicator for this converter.
*
Expand All @@ -79,14 +90,42 @@ class Converter {

/**
* Instance alias for the showProgress flag so consumers don't need to reach
* into the constructor directly.
* into the constructor directly. Can be overridden by subclasses to allow instance-level
* control over progress display.
*
* @return {boolean} True if this instance should show progress, false otherwise.
*/
get showProgress() {
if ( typeof this.options.showProgress === 'boolean' ) {
return this.options.showProgress
}
return this.constructor.showProgress
}

/**
* Delay before showing the progress indicator, in milliseconds. If the conversion is expected to be very fast
* the delay allows the progress modal to be skipped.
*
* @return {number} Delay in milliseconds before showing the progress indicator.
*/
static get progressDelay() {
return 0
}

/**
* Instance alias for the progressDelay so consumers don't need to reach
* into the constructor directly. Can be overridden by subclasses to allow instance-level
* control over progress delay.
*
* @return {number} Delay in milliseconds before showing the progress indicator.
*/
get progressDelay() {
if ( typeof this.options.progressDelay === 'number' ) {
return this.options.progressDelay
}
return this.constructor.progressDelay
}

/**
* Perform conversion/optimization.
* Subclasses must implement this method.
Expand All @@ -96,6 +135,15 @@ class Converter {
throw new Error( 'convert() must be implemented by subclass' )
}

/**
* Perform smart optimization.
* If a subclass has not implemented this method, perform regular conversion.
* @return {Promise<{file: File|Blob, metadata?: Object}>} Promise resolving with the converted file and optional metadata.
*/
async optimize() {
return await this.convert()
}

/**
* Cancel the current conversion.
* Subclasses should override this to implement actual cancellation logic.
Expand Down
Loading
Loading