diff --git a/functions.php b/functions.php index cbc5975..ba85a7e 100644 --- a/functions.php +++ b/functions.php @@ -4,7 +4,7 @@ * * @package zeitfresser */ - + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -17,10 +17,68 @@ if ( ! defined( 'DAISY_BLOG_VERSION' ) ) { define( 'DAISY_BLOG_VERSION', ZEITFRESSER_VERSION ); } +if ( ! defined( 'ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION' ) ) { + define( 'ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION', '1.0' ); +} + require get_template_directory() . '/inc/zeitfresser-helpers.php'; require get_template_directory() . '/inc/performance-tools.php'; require get_template_directory() . '/inc/zeitfresser-toc.php'; +/** + * Upload Handler + */ +add_filter('wp_handle_upload', 'zeitfresser_capture_original_upload', 10, 2); + +/** + * Capture original upload path safely + */ +function zeitfresser_capture_original_upload( $upload, $context ) { + + if ( empty( $upload['file'] ) ) { + return $upload; + } + + // Store temporarily (request-scoped) + $GLOBALS['zeitfresser_last_uploaded_file'] = $upload['file']; + + return $upload; +} +add_filter( 'wp_handle_upload', 'zeitfresser_capture_original_upload', 10, 2 ); + +/** + * Persist original file path to attachment meta + */ +function zeitfresser_store_original_file( $attachment_id ) { + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + return; + } + + if ( empty( $GLOBALS['zeitfresser_last_uploaded_file'] ) ) { + return; + } + + $file = $GLOBALS['zeitfresser_last_uploaded_file']; + + // Safety: ensure file still exists + if ( ! file_exists( $file ) ) { + return; + } + + // Prevent overwrite if already set + if ( get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) { + return; + } + + update_post_meta( + $attachment_id, + '_zeitfresser_original_file', + $file + ); +} +add_action( 'add_attachment', 'zeitfresser_store_original_file' ); + /** * Theme setup. * @@ -68,6 +126,19 @@ function zeitfresser_setup() { } add_action( 'after_setup_theme', 'zeitfresser_setup' ); +/** + * Register optimized image sizes + */ +function zeitfresser_custom_image_sizes() { + + // Content images (main article) + add_image_size( 'zeitfresser-content', 720, 0, false ); + + // Archive / card layout + add_image_size( 'zeitfresser-card', 480, 0, false ); +} +add_action( 'after_setup_theme', 'zeitfresser_custom_image_sizes' ); + /** * Set the content width in pixels. * @@ -268,21 +339,34 @@ function zeitfresser_cleanup_wp_head() { add_action( 'init', 'zeitfresser_cleanup_wp_head' ); /** - * Preload local fonts for better performance + * Preload critical local fonts only */ function zeitfresser_preload_fonts() { ?> + - - - - get_template_directory_uri(), + 'crossorigin' => 'anonymous', + ]; + } + + return $urls; + +}, 10, 2 ); + /** * Remove front-end dashicons for visitors. * @@ -317,13 +401,18 @@ function zeitfresser_optimize_image_attributes( $attr, $attachment, $size ) { add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_optimize_image_attributes', 10, 3 ); /** - * Lower the threshold for WordPress scaled originals. + * Lower the threshold for WordPress scaled originals when auto optimization is enabled. * - * This prevents very large uploads from shipping oversized source images. + * When automatic optimization is disabled, original uploads should remain untouched. * - * @return int + * @return int|false */ function zeitfresser_big_image_size_threshold() { + + if ( ! get_theme_mod( 'ztfr_auto_optimize', true ) ) { + return false; + } + return 1800; } add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' ); @@ -335,28 +424,184 @@ add_filter( 'big_image_size_threshold', 'zeitfresser_big_image_size_threshold' ) * @return array */ function zeitfresser_filter_intermediate_image_sizes( $sizes ) { - unset( $sizes['1536x1536'], $sizes['2048x2048'] ); + + // Remove oversized defaults + unset( + $sizes['1536x1536'], + $sizes['2048x2048'] + ); return $sizes; } add_filter( 'intermediate_image_sizes_advanced', 'zeitfresser_filter_intermediate_image_sizes' ); /** - * Convert generated JPEG and PNG sub-sizes to WebP when supported by the server. + * Convert generated JPEG and PNG files to AVIF/WebP when enabled. + * + * Auto optimization can be disabled for uploads via Customizer. + * Manual optimization may still force conversion through a request-scoped flag. * * @param array $formats Output format map. * @return array */ function zeitfresser_image_output_format( $formats ) { - if ( function_exists( 'wp_image_editor_supports' ) && wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) { - $formats['image/jpeg'] = 'image/webp'; - $formats['image/png'] = 'image/webp'; + + $auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true ); + $force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] ); + + if ( ! $auto_enabled && ! $force_enabled ) { + return $formats; + } + + if ( function_exists( 'wp_image_editor_supports' ) ) { + + // Prefer AVIF if supported. + if ( wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) { + $formats['image/jpeg'] = 'image/avif'; + $formats['image/png'] = 'image/avif'; + + // Fallback to WebP. + } elseif ( wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) { + $formats['image/jpeg'] = 'image/webp'; + $formats['image/png'] = 'image/webp'; + } } return $formats; } add_filter( 'image_editor_output_format', 'zeitfresser_image_output_format' ); +/** + * Mark images as optimized only when optimization is actually active. + * + * @param array $metadata Attachment metadata. + * @param int $attachment_id Attachment ID. + * @return array + */ +function zeitfresser_mark_new_images_optimized( $metadata, $attachment_id ) { + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + return $metadata; + } + + $auto_enabled = get_theme_mod( 'ztfr_auto_optimize', true ); + $force_enabled = ! empty( $GLOBALS['zeitfresser_force_image_optimization'] ); + + if ( ! $auto_enabled && ! $force_enabled ) { + return $metadata; + } + + update_post_meta( + $attachment_id, + '_zeitfresser_media_optimized_version', + ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION + ); + + return $metadata; +} +add_filter( 'wp_generate_attachment_metadata', 'zeitfresser_mark_new_images_optimized', 20, 2 ); + +/** + * Auto Optimize Hook + */ +add_filter( + 'wp_generate_attachment_metadata', + 'zeitfresser_auto_optimize_on_upload', + 15, + 2 +); + +function zeitfresser_auto_optimize_on_upload( $metadata, $attachment_id ) { + + // ๐ only images + if ( ! wp_attachment_is_image( $attachment_id ) ) { + return $metadata; + } + + // ๐ feature toggle + if ( ! get_theme_mod( 'ztfr_auto_optimize', true ) ) { + return $metadata; + } + + $file = get_attached_file( $attachment_id ); + + if ( ! $file || ! file_exists( $file ) ) { + return $metadata; + } + + // ๐ฅ DO NOT overwrite upload-captured original + if ( ! get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) { + + // fallback only + update_post_meta( $attachment_id, '_zeitfresser_original_file', $file ); + } + + // mark as optimized + update_post_meta( + $attachment_id, + '_zeitfresser_media_optimized_version', + ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION + ); + + return $metadata; +} + +/** + * Auto Delete Hook + */ +add_filter( + 'wp_generate_attachment_metadata', + 'zeitfresser_auto_delete_original_after_upload', + 30, + 2 +); + +function zeitfresser_auto_delete_original_after_upload( $metadata, $attachment_id ) { + + // ๐ only images + if ( ! wp_attachment_is_image( $attachment_id ) ) { + return $metadata; + } + + // ๐ feature toggles + if ( ! get_theme_mod( 'ztfr_auto_optimize', true ) ) { + return $metadata; + } + + if ( ! get_theme_mod( 'ztfr_auto_delete', false ) ) { + return $metadata; + } + + $original = get_post_meta( + $attachment_id, + '_zeitfresser_original_file', + true + ); + + if ( ! $original || ! file_exists( $original ) ) { + return $metadata; + } + + // skip modern formats + $ext = strtolower( pathinfo( $original, PATHINFO_EXTENSION ) ); + + if ( in_array( $ext, [ 'webp', 'avif' ], true ) ) { + update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 ); + return $metadata; + } + + if ( ! is_writable( $original ) ) { + return $metadata; + } + + // ๐ฅ delete original + if ( unlink( $original ) ) { + update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 ); + } + + return $metadata; +} + /** * Keep generated image quality balanced for file size and visual fidelity. * @@ -365,11 +610,18 @@ add_filter( 'image_editor_output_format', 'zeitfresser_image_output_format' ); * @return int */ function zeitfresser_image_quality( $quality, $mime_type = 'image/jpeg' ) { - if ( 'image/png' === $mime_type ) { - return $quality; - } - return 82; + switch ( $mime_type ) { + case 'image/avif': + return 50; + + case 'image/webp': + return 75; + + case 'image/jpeg': + default: + return 82; + } } add_filter( 'wp_editor_set_quality', 'zeitfresser_image_quality', 10, 2 ); @@ -399,7 +651,7 @@ function zeitfresser_improve_attachment_dimensions( $attr, $attachment, $size ) if ( empty( $attr['fetchpriority'] ) && ! is_admin() && ! is_feed() ) { static $did_set_high_priority = false; - if ( ! $did_set_high_priority && ( is_home() || is_front_page() || is_archive() || is_search() || is_singular() ) ) { + if ( ! $did_set_high_priority && is_singular() ) { $attr['fetchpriority'] = 'high'; $did_set_high_priority = true; } @@ -409,6 +661,62 @@ function zeitfresser_improve_attachment_dimensions( $attr, $attachment, $size ) } add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachment_dimensions', 11, 3 ); +/** + * Improve responsive image sizes attribute. + * + * @param string $sizes Existing sizes attribute. + * @param array $size Requested image size. + * @return string + */ +function zeitfresser_responsive_image_sizes( $sizes, $size ) { + + // Single post content + if ( is_singular() ) { + return '(max-width: 768px) 100vw, (max-width: 1200px) 720px, 720px'; + } + + // Archive / blog overview + if ( is_home() || is_front_page() || is_archive() || is_search() ) { + return '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 400px'; + } + + return $sizes; +} +add_filter( 'wp_calculate_image_sizes', 'zeitfresser_responsive_image_sizes', 10, 2 ); + +/** + * Determine the most likely LCP image URL. + * + * @return string + */ +function zeitfresser_get_lcp_image_url() { + + // Single posts/pages โ featured image + if ( is_singular() ) { + $post_id = get_queried_object_id(); + + if ( $post_id && has_post_thumbnail( $post_id ) ) { + return get_the_post_thumbnail_url( $post_id, 'large' ); + } + } + + // Archives / homepage โ first post with thumbnail + if ( is_home() || is_front_page() || is_archive() || is_search() ) { + + global $wp_query; + + if ( ! empty( $wp_query->posts ) ) { + + foreach ( $wp_query->posts as $post ) { + if ( has_post_thumbnail( $post->ID ) ) { + return get_the_post_thumbnail_url( $post->ID, 'medium_large' ); + } + } + } + } + + return ''; +} /** * Preload the most likely LCP image for archive and singular views. @@ -417,39 +725,35 @@ add_filter( 'wp_get_attachment_image_attributes', 'zeitfresser_improve_attachmen * @return array */ function zeitfresser_preload_resources( $resources ) { + if ( is_admin() || is_feed() || is_embed() ) { return $resources; } - $image_url = ''; - $image_type = ''; - - if ( is_singular() ) { - $object_id = get_queried_object_id(); - - if ( $object_id && has_post_thumbnail( $object_id ) ) { - $image_url = get_the_post_thumbnail_url( $object_id, 'large' ); - } - } elseif ( is_home() || is_front_page() || is_archive() || is_search() ) { - global $wp_query; - - if ( isset( $wp_query->posts[0]->ID ) && has_post_thumbnail( $wp_query->posts[0]->ID ) ) { - $image_url = get_the_post_thumbnail_url( $wp_query->posts[0]->ID, 'thumbnail' ); - } - } + // ๐ฅ NEW: use smart detection + $image_url = zeitfresser_get_lcp_image_url(); if ( empty( $image_url ) ) { return $resources; } - $extension = strtolower( pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ), PATHINFO_EXTENSION ) ); + // Robust extension detection + $extension = strtolower( + pathinfo( + wp_parse_url( $image_url, PHP_URL_PATH ), + PATHINFO_EXTENSION + ) + ); - if ( 'jpg' === $extension || 'jpeg' === $extension ) { - $image_type = 'image/jpeg'; - } elseif ( 'png' === $extension ) { + // MIME fallback + $image_type = 'image/jpeg'; + + if ( 'png' === $extension ) { $image_type = 'image/png'; } elseif ( 'webp' === $extension ) { $image_type = 'image/webp'; + } elseif ( 'avif' === $extension ) { + $image_type = 'image/avif'; } $resources[] = array_filter( diff --git a/inc/customizer.php b/inc/customizer.php index 7c4d630..b887c6e 100644 --- a/inc/customizer.php +++ b/inc/customizer.php @@ -9,8 +9,10 @@ * Add postMessage support for site title and description for the Theme Customizer. * * @param WP_Customize_Manager $wp_customize Theme Customizer object. + * @return void */ function zeitfresser_customize_register( $wp_customize ) { + $wp_customize->get_setting( 'blogname' )->transport = 'postMessage'; $wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage'; $wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage'; @@ -23,6 +25,7 @@ function zeitfresser_customize_register( $wp_customize ) { 'render_callback' => 'zeitfresser_customize_partial_blogname', ) ); + $wp_customize->selective_refresh->add_partial( 'blogdescription', array( @@ -31,6 +34,59 @@ function zeitfresser_customize_register( $wp_customize ) { ) ); } + + /** + * Performance Tools section + */ + $wp_customize->add_section( + 'ztfr_performance_tools', + array( + 'title' => 'Performance Tools Settings', + 'priority' => 160, + ) + ); + + /** + * Auto optimize uploaded images. + */ + $wp_customize->add_setting( + 'ztfr_auto_optimize', + array( + 'default' => true, + 'sanitize_callback' => 'wp_validate_boolean', + ) + ); + + $wp_customize->add_control( + 'ztfr_auto_optimize', + array( + 'type' => 'checkbox', + 'section' => 'ztfr_performance_tools', + 'label' => 'Auto Optimize Pictures on Upload', + 'description' => 'Automatically converts uploaded images to modern formats (AVIF/WebP) for improved performance.', + ) + ); + + /** + * Auto delete originals after successful optimization. + */ + $wp_customize->add_setting( + 'ztfr_auto_delete', + array( + 'default' => false, + 'sanitize_callback' => 'wp_validate_boolean', + ) + ); + + $wp_customize->add_control( + 'ztfr_auto_delete', + array( + 'type' => 'checkbox', + 'section' => 'ztfr_performance_tools', + 'label' => 'Auto Delete Original Pictures on Upload', + 'description' => 'Automatically deletes original images after optimization. Warning: deleting original images cannot be undone.', + ) + ); } add_action( 'customize_register', 'zeitfresser_customize_register' ); @@ -53,9 +109,130 @@ function zeitfresser_customize_partial_blogdescription() { } /** - * Binds JS handlers to make Theme Customizer preview reload changes asynchronously. + * Bind JS handlers to make Theme Customizer preview reload changes asynchronously. + * + * @return void */ function zeitfresser_customize_preview_js() { - wp_enqueue_script( 'zeitfresser-customizer', get_template_directory_uri() . '/js/customizer.js', array( 'customize-preview' ), ZEITFRESSER_VERSION, true ); + wp_enqueue_script( + 'zeitfresser-customizer', + get_template_directory_uri() . '/js/customizer.js', + array( 'customize-preview' ), + ZEITFRESSER_VERSION, + true + ); } add_action( 'customize_preview_init', 'zeitfresser_customize_preview_js' ); + +/** + * Add dependency logic and status box for Performance Tools settings. + * + * @return void + */ +function zeitfresser_customize_controls_dependency_js() { + ?> + + 'attachment', - 'post_status' => 'inherit', - 'post_mime_type' => 'image', - 'fields' => 'ids', - 'posts_per_page' => 1, - 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - array( - 'key' => '_zeitfresser_media_optimized', - 'compare' => 'NOT EXISTS', - ), - ), - 'no_found_rows' => false, - 'cache_results' => false, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - ) - ); + $query = new WP_Query([ + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'post_mime_type' => 'image', + 'fields' => 'ids', + 'posts_per_page' => 1, + 'meta_query' => [ + 'relation' => 'OR', + [ + 'key' => '_zeitfresser_media_optimized_version', + 'compare' => 'NOT EXISTS', + ], + [ + 'key' => '_zeitfresser_media_optimized_version', + 'value' => ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION, + 'compare' => '!=', + ], + ], + 'no_found_rows' => false, + ]); - return (int) $query->found_posts; + return (int) $query->found_posts; } /** - * Process a batch of legacy images with current thumbnail/webp rules. + * Count total images + */ +function zeitfresser_get_total_images_count() { + $query = new WP_Query([ + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'post_mime_type' => 'image', + 'fields' => 'ids', + 'posts_per_page' => 1, + 'no_found_rows' => false, + ]); + + return (int) $query->found_posts; +} + +/** + * NEW: Cleanup counters (ONLY ADDITIVE) + */ +function zeitfresser_get_total_originals_count() { + $query = new WP_Query([ + 'post_type'=>'attachment', + 'post_status'=>'inherit', + 'post_mime_type'=>'image', + 'posts_per_page'=>1, + 'fields'=>'ids', + 'meta_query'=>[ + ['key'=>'_zeitfresser_original_file','compare'=>'EXISTS'] + ], + 'no_found_rows'=>false + ]); + return (int) $query->found_posts; +} + +function zeitfresser_get_remaining_originals_count() { + $query = new WP_Query([ + 'post_type'=>'attachment', + 'post_status'=>'inherit', + 'post_mime_type'=>'image', + 'posts_per_page'=>1, + 'fields'=>'ids', + 'meta_query'=>[ + 'relation'=>'AND', + ['key'=>'_zeitfresser_original_file','compare'=>'EXISTS'], + ['key'=>'_zeitfresser_original_deleted','compare'=>'NOT EXISTS'] + ], + 'no_found_rows'=>false + ]); + return (int) $query->found_posts; +} + +/** + * DELETE ORIGINALS (UNCHANGED) + */ +function zeitfresser_delete_originals_batch( $batch_size = 10 ) { + + $deleted = 0; + + $query = new WP_Query([ + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'post_mime_type' => 'image', + 'fields' => 'ids', + 'posts_per_page' => $batch_size, + 'meta_query' => [ + 'relation' => 'AND', + [ + 'key' => '_zeitfresser_original_file', + 'compare' => 'EXISTS', + ], + [ + 'key' => '_zeitfresser_original_deleted', + 'compare' => 'NOT EXISTS', + ], + ], + ]); + + foreach ( $query->posts as $attachment_id ) { + + $original = get_post_meta( $attachment_id, '_zeitfresser_original_file', true ); + + if ( ! $original ) { + continue; + } + + $optimized_version = get_post_meta( + $attachment_id, + '_zeitfresser_media_optimized_version', + true + ); + + if ( ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION !== $optimized_version ) { + continue; + } + + $ext = strtolower( pathinfo( $original, PATHINFO_EXTENSION ) ); + + if ( in_array( $ext, [ 'webp', 'avif' ], true ) ) { + update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 ); + continue; + } + + if ( ! file_exists( $original ) ) { + update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 ); + continue; + } + + if ( ! is_writable( $original ) ) { + continue; + } + + if ( unlink( $original ) ) { + $deleted++; + update_post_meta( $attachment_id, '_zeitfresser_original_deleted', 1 ); + } + } + + return $deleted; +} + +/** + * Optimizer batch for manual processing. * - * @param int $batch_size Number of images to process. + * Manual optimization must work independently of the auto-optimize upload toggle. + * + * @param int $batch_size Number of images per batch. * @return array */ -function zeitfresser_process_legacy_images_batch( $batch_size = 20 ) { - $results = array( - 'processed' => 0, - 'updated' => 0, - 'skipped' => 0, - 'errors' => 0, - ); +function zeitfresser_process_legacy_images_batch( $batch_size = 25 ) { - $query = new WP_Query( - array( - 'post_type' => 'attachment', - 'post_status' => 'inherit', - 'post_mime_type' => 'image', - 'fields' => 'ids', - 'posts_per_page' => absint( $batch_size ), - 'orderby' => 'ID', - 'order' => 'ASC', - 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - array( - 'key' => '_zeitfresser_media_optimized', - 'compare' => 'NOT EXISTS', - ), - ), - 'no_found_rows' => true, - 'cache_results' => false, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - ) - ); + $results = [ + 'processed' => 0, + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + ]; - if ( empty( $query->posts ) ) { - return $results; - } + $query = new WP_Query([ + 'post_type' => 'attachment', + 'post_status' => 'inherit', + 'post_mime_type' => 'image', + 'fields' => 'ids', + 'posts_per_page' => $batch_size, + 'meta_query' => [ + 'relation' => 'OR', + [ + 'key' => '_zeitfresser_media_optimized_version', + 'compare' => 'NOT EXISTS', + ], + [ + 'key' => '_zeitfresser_media_optimized_version', + 'value' => ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION, + 'compare' => '!=', + ], + ], + ]); - foreach ( $query->posts as $attachment_id ) { - $results['processed']++; + // Force optimization for manual tool runs, regardless of upload automation setting. + $GLOBALS['zeitfresser_force_image_optimization'] = true; - $file = get_attached_file( $attachment_id ); + foreach ( $query->posts as $attachment_id ) { - if ( empty( $file ) || ! file_exists( $file ) ) { - update_post_meta( $attachment_id, '_zeitfresser_media_optimized', 'missing' ); - $results['skipped']++; - continue; - } + $results['processed']++; - $metadata = wp_generate_attachment_metadata( $attachment_id, $file ); + $file = get_attached_file( $attachment_id ); - if ( is_wp_error( $metadata ) || empty( $metadata ) ) { - $results['errors']++; - continue; - } + if ( empty( $file ) || ! file_exists( $file ) ) { + update_post_meta( $attachment_id, '_zeitfresser_media_optimized_version', 'missing' ); + $results['skipped']++; + continue; + } - wp_update_attachment_metadata( $attachment_id, $metadata ); - update_post_meta( $attachment_id, '_zeitfresser_media_optimized', current_time( 'mysql' ) ); - $results['updated']++; - } + if ( ! get_post_meta( $attachment_id, '_zeitfresser_original_file', true ) ) { + update_post_meta( $attachment_id, '_zeitfresser_original_file', $file ); + } - return $results; + $metadata = wp_generate_attachment_metadata( $attachment_id, $file ); + + if ( is_wp_error( $metadata ) || empty( $metadata ) ) { + $results['errors']++; + continue; + } + + wp_update_attachment_metadata( $attachment_id, $metadata ); + + update_post_meta( + $attachment_id, + '_zeitfresser_media_optimized_version', + ZEITFRESSER_IMAGE_OPTIMIZATION_VERSION + ); + + $results['updated']++; + } + + unset( $GLOBALS['zeitfresser_force_image_optimization'] ); + + return $results; } /** - * Handle admin actions for performance tools. - * - * @return void + * AJAX: Optimizer (extended output only) */ -function zeitfresser_handle_performance_tools_actions() { - if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) { - return; - } +function zeitfresser_ajax_optimize_images() { - if ( empty( $_GET['page'] ) || 'zeitfresser-performance-tools' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error(); + } - if ( empty( $_GET['zeitfresser_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } + check_ajax_referer( 'zeitfresser_performance_tools', 'nonce' ); - check_admin_referer( 'zeitfresser_performance_tools' ); + $results = zeitfresser_process_legacy_images_batch( 25 ); - $action = sanitize_key( wp_unslash( $_GET['zeitfresser_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( 'optimize_legacy_images' === $action ) { - $results = zeitfresser_process_legacy_images_batch( 25 ); - $args = array( - 'page' => 'zeitfresser-performance-tools', - 'processed' => $results['processed'], - 'updated' => $results['updated'], - 'skipped' => $results['skipped'], - 'errors' => $results['errors'], - ); - - wp_safe_redirect( add_query_arg( $args, admin_url( 'themes.php' ) ) ); - exit; - } - - if ( 'reset_legacy_images' === $action ) { - $query = new WP_Query( - array( - 'post_type' => 'attachment', - 'post_status' => 'inherit', - 'post_mime_type' => 'image', - 'fields' => 'ids', - 'posts_per_page' => -1, - 'meta_key' => '_zeitfresser_media_optimized', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - 'no_found_rows' => true, - 'cache_results' => false, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - ) - ); - - foreach ( $query->posts as $attachment_id ) { - delete_post_meta( $attachment_id, '_zeitfresser_media_optimized' ); - } - - wp_safe_redirect( - add_query_arg( - array( - 'page' => 'zeitfresser-performance-tools', - 'reset' => count( $query->posts ), - ), - admin_url( 'themes.php' ) - ) - ); - exit; - } + wp_send_json_success([ + 'processed' => $results['processed'], + 'updated' => $results['updated'], + 'pending' => zeitfresser_get_pending_legacy_images_count(), + 'total' => zeitfresser_get_total_images_count(), + ]); } -add_action( 'admin_init', 'zeitfresser_handle_performance_tools_actions' ); +add_action( 'wp_ajax_zeitfresser_optimize_images', 'zeitfresser_ajax_optimize_images' ); /** - * Render the performance tools page. - * - * @return void + * AJAX: Delete (extended ONLY) + */ +function zeitfresser_ajax_delete_originals() { + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error(); + } + + check_ajax_referer( 'zeitfresser_performance_tools', 'nonce' ); + + $deleted = zeitfresser_delete_originals_batch( 10 ); + + $total = zeitfresser_get_total_originals_count(); + $remaining = zeitfresser_get_remaining_originals_count(); + + wp_send_json_success([ + 'deleted' => $deleted, + 'total' => $total, + 'remaining' => $remaining, + 'deleted_total' => $total - $remaining, + ]); +} +add_action( 'wp_ajax_zeitfresser_delete_originals', 'zeitfresser_ajax_delete_originals' ); + +/** + * Render UI */ function zeitfresser_render_performance_tools_page() { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - $pending = zeitfresser_get_pending_legacy_images_count(); - $local = function_exists( 'zeitfresser_get_local_webfonts_css' ) ? zeitfresser_get_local_webfont_urls( zeitfresser_get_local_webfonts_css() ) : array(); - ?> -
- -
-- -
-
+ How this tool works
+
+ This tool helps you optimize your existing media library for better performance.
+
+ โข Images are converted to modern formats (AVIF/WebP) for smaller file sizes.
+ โข The original file path is safely stored before optimization.
+ โข Once optimized, original images can be deleted to save disk space.
+
+ Automation:
+ โข You can enable automatic optimization on upload in the Customizer under Performance Tools Settings.
+ โข Optionally, original images can also be deleted automatically after successful optimization.
+
+ Safety:
+ โข Images are only processed once per version.
+ โข Original files are only deleted when safe.
+ โข The tool can be run multiple times without side effects.
+
+ Tip: You can either automate the process via the Customizer or use this tool manually for full control.
+
Total Images:
+Optimized:
+Pending:
+Progress: %
+Total Originals:
+Deleted:
+Remaining:
+Cleanup Progress: %
+๐ Optimizer: Idle
+๐งน Cleanup: Idle
+