Changeset 3798740
- Timestamp:
- 11/12/2026 04:08:00 PM (15 hours ago)
- Location:
- enfants-img-performer/trunk
- Files:
-
- 6 edited
-
CHANGELOG.md (modified) (1 diff)
-
enfants-img-performer.php (modified) (2 diffs)
-
includes/Admin/AiLabelingSettings.php (modified) (2 diffs)
-
includes/Admin/Settings.php (modified) (2 diffs)
-
includes/Core/AiLabelBadge.php (modified) (11 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
enfants-img-performer/trunk/CHANGELOG.md
r3786650 r3798740 2 2 3 3 Alle relevanten Änderungen an IMG Performer sind hier dokumentiert. 4 5 ## [0.3.16] – 2026-11-12 6 7 ### Neu 8 - Floating Label als zweites Design für das sichtbare KI-Label: In Ruhe nur eine kleine „AI“-Pille, bei Hover, Fokus oder Tippen wächst sie weich zu „AI generated“ bzw. „AI modified“ auf. Bedienbar per Maus, Tastatur (Enter, Leertaste, Esc) und Touch, mit `aria-expanded` für Screenreader; bei „Bewegung reduzieren“ ohne Animation (IMP-315) 9 - Einstellungen unter „Visible label on the frontend“: Design (Circle oder Floating) und Position (oben links oder unten rechts) (IMP-315) 10 - Labels passen sich der Bildbreite an: Auf Bildern unter 200 px wird das Label kleiner (70 %) und sitzt näher an der Ecke (IMP-316) 11 12 ### Geändert 13 - Das Circle Label ist etwas kleiner (IMP-315) 14 15 ### Behoben 16 - Beitragsbilder bekamen zwei Label-Ebenen übereinander. Bei Circle fiel das nicht auf, beim Floating Label schien die zweite Ebene durch (IMP-315) 17 - Beitragsbild im Cover-Block (Hero): Mit KI-Label wurde das Bild abgeschnitten und der Hero blieb leer. Das Bild füllt den Cover wieder vollständig, und das Label liegt über dem Abdunkeln (Dim) des Cover-Blocks, auch bei Cover-Blöcken mit festem Hintergrund (IMP-315) 4 18 5 19 ## [0.3.15] – 2026-11-05 -
enfants-img-performer/trunk/enfants-img-performer.php
r3786650 r3798740 3 3 * Plugin Name: Enfants IMG Performer 4 4 * Description: Converts uploaded images to WebP automatically and detects and labels AI-generated images on upload automatically, with manual classification as a complement. Runs on your own server, no cloud, no account. Designed for GDPR. 5 * Version: 0.3.1 55 * Version: 0.3.16 6 6 * Author: Enfants Terribles digital GmbH 7 7 * Author URI: https://www.enfants.de … … 18 18 define( 'EIMP_PATH', plugin_dir_path( __FILE__ ) ); 19 19 define( 'EIMP_URL', plugin_dir_url( __FILE__ ) ); 20 define( 'EIMP_VERSION', '0.3.1 5' );20 define( 'EIMP_VERSION', '0.3.16' ); 21 21 22 22 /** -
enfants-img-performer/trunk/includes/Admin/AiLabelingSettings.php
r3772693 r3798740 49 49 ] 50 50 ); 51 register_setting( 52 'eimp_ai_labeling_settings', 53 'eimp_ai_label_design', 54 [ 55 'type' => 'string', 56 'sanitize_callback' => static function ( $value ): string { 57 return in_array( $value, [ 'circle', 'floating' ], true ) ? $value : 'circle'; 58 }, 59 'default' => 'circle', 60 ] 61 ); 62 // PRO registers eimp_ai_label_position itself (all four corners). 63 if ( ! (bool) apply_filters( 'eimp_is_pro_active', false ) ) { 64 register_setting( 65 'eimp_ai_labeling_settings', 66 'eimp_ai_label_position', 67 [ 68 'type' => 'string', 69 'sanitize_callback' => static function ( $value ): string { 70 return in_array( $value, [ 'top-left', 'bottom-right' ], true ) ? $value : 'top-left'; 71 }, 72 'default' => 'top-left', 73 ] 74 ); 75 } 76 } 77 78 /** 79 * Free design + position fields (IMP-315). PRO 80 * replaces these with its own, richer controls. 81 */ 82 private function render_free_label_fields() { 83 $design = (string) get_option( 'eimp_ai_label_design', 'circle' ); 84 $position = (string) get_option( 'eimp_ai_label_position', 'top-left' ); 85 $design = in_array( $design, [ 'circle', 'floating' ], true ) ? $design : 'circle'; 86 $position = in_array( $position, [ 'top-left', 'bottom-right' ], true ) ? $position : 'top-left'; 87 ?> 88 <div style="display:flex; gap:32px; flex-wrap:wrap; margin-top:20px;"> 89 <div> 90 <fieldset style="margin:0 0 16px; border:0; padding:0;"> 91 <legend style="font-weight:600; margin-bottom:6px;"><?php esc_html_e( 'Design', 'enfants-img-performer' ); ?></legend> 92 <label style="display:block; margin-bottom:4px;"><input type="radio" name="eimp_ai_label_design" value="circle" <?php checked( 'circle', $design ); ?> /> <?php esc_html_e( 'Circle Label', 'enfants-img-performer' ); ?></label> 93 <label style="display:block;"><input type="radio" name="eimp_ai_label_design" value="floating" <?php checked( 'floating', $design ); ?> /> <?php esc_html_e( 'Floating Label', 'enfants-img-performer' ); ?></label> 94 </fieldset> 95 <fieldset style="margin:0; border:0; padding:0;"> 96 <legend style="font-weight:600; margin-bottom:6px;"><?php esc_html_e( 'Position', 'enfants-img-performer' ); ?></legend> 97 <label style="display:block; margin-bottom:4px;"><input type="radio" name="eimp_ai_label_position" value="top-left" <?php checked( 'top-left', $position ); ?> /> <?php esc_html_e( 'Top left', 'enfants-img-performer' ); ?></label> 98 <label style="display:block; margin-bottom:4px;"><input type="radio" name="eimp_ai_label_position" value="bottom-right" <?php checked( 'bottom-right', $position ); ?> /> <?php esc_html_e( 'Bottom right', 'enfants-img-performer' ); ?></label> 99 </fieldset> 100 </div> 101 </div> 102 <?php 51 103 } 52 104 … … 104 156 * @param int $enabled Current value of eimp_ai_label_enabled. 105 157 */ 158 if ( ! (bool) apply_filters( 'eimp_is_pro_active', false ) ) { 159 $this->render_free_label_fields(); 160 } 106 161 do_action( 'eimp_ai_label_extra_fields', $enabled ); 107 162 ?> -
enfants-img-performer/trunk/includes/Admin/Settings.php
r3741830 r3798740 116 116 */ 117 117 private function render_quality_field() { 118 $value = (int) get_option( 'eimp_quality', 85 ); 119 $pct = (int) round( ( $value - 1 ) / 99 * 100 ); 118 $value = (int) get_option( 'eimp_quality', 85 ); 119 $pct = (int) round( ( $value - 1 ) / 99 * 100 ); 120 $is_pro = (bool) apply_filters( 'eimp_is_pro_active', false ); 120 121 ?> 121 122 <div style="display:flex; align-items:center; gap:12px; margin-bottom:6px; white-space:nowrap;"> 122 <span style="font-size:12px; font-weight:600;"><?php e sc_html_e( 'Quality', 'enfants-img-performer' ); ?></span>123 <span style="font-size:12px; font-weight:600;"><?php echo $is_pro ? esc_html__( 'WebP quality', 'enfants-img-performer' ) : esc_html__( 'Quality', 'enfants-img-performer' ); ?></span> 123 124 <span style="font-size:11px; color:#666670;"><?php esc_html_e( 'Recommended: 70–85', 'enfants-img-performer' ); ?></span> 124 125 </div> … … 490 491 </div> 491 492 <div style="display:flex; gap:90px; flex-wrap:wrap; margin-bottom:24px;"> 492 <div><?php $this->render_quality_field(); ?></div> 493 <div> 494 <?php 495 $this->render_quality_field(); 496 /** 497 * Lets PRO add its AVIF quality slider under the WebP slider, inside 498 * this form so both share the one Save button (IMP-326). 499 */ 500 do_action( 'eimp_after_quality_field' ); 501 ?> 502 </div> 493 503 <div style="flex:1 0 0; min-width:240px;"><?php $this->render_stats_opt_in_field(); ?></div> 494 504 </div> -
enfants-img-performer/trunk/includes/Core/AiLabelBadge.php
r3786650 r3798740 83 83 // there's nothing for that margin to usefully do here anyway. 84 84 . '.eimp-ai-badge-wrap img{display:block;margin:0}' 85 // Floating Label (IMP-315): small "AI" pill at rest, grows into 86 // the full status pill when expanded. Without JS: hover/focus. 87 // With JS (see print_floating_label_js) aria-expanded drives it, 88 // so Esc/Enter/tap work. prefers-reduced-motion: no animation. 89 . '.eimp-ai-badge--floating{cursor:default;outline-offset:2px}' 90 . '.eimp-ai-badge--floating svg{display:block}' 91 // Soft transition (IMP-315, design by Alex): the pill grows from 92 // 35px to full width while the text fades in along a soft mask 93 // edge. Only one SVG is visible at a time; the large one is 94 // offset by 1.5px/-.156px so "AI" sits exactly on the small one. 95 // Pill colour comes from the badge (--imgp-bg), because the mask 96 // would otherwise fade the SVG's own pill. Pure CSS, no library. 97 . '@property --imgp-p{syntax:"<length>";inherits:false;initial-value:24px}' 98 . '.eimp-ai-badge--floating{--imgp-rest:35px;--imgp-full:105.5px;--imgp-bg:#fff;position:absolute;display:block;overflow:hidden;border-radius:10px;height:20px;width:var(--imgp-rest);background:var(--imgp-bg);contain:layout paint;transition:width calc(var(--imgp-k,1)*var(--imgp-dc,460ms)*.85) var(--imgp-ease-c,cubic-bezier(.45,0,.2,1)) calc(var(--imgp-k,1)*var(--imgp-dc,460ms)*.15)}' 99 . '.eimp-ai-badge--floating>span{position:absolute;top:0;left:0;display:block}' 100 . '.eimp-ai-badge--floating .eimp-ai-badge__rest{opacity:1;transition:opacity 0s calc(var(--imgp-k,1)*var(--imgp-dc,460ms))}' 101 . '.eimp-ai-badge--floating .eimp-ai-badge__full{opacity:0;transform:translate(1.5px,-.156px);--imgp-p:24px;' 102 . '-webkit-mask-image:linear-gradient(90deg,#000 23.5px,transparent 23.5px),linear-gradient(90deg,#000 calc(var(--imgp-p) - var(--imgp-soft,24px)),transparent var(--imgp-p));' 103 . 'mask-image:linear-gradient(90deg,#000 23.5px,transparent 23.5px),linear-gradient(90deg,#000 calc(var(--imgp-p) - var(--imgp-soft,24px)),transparent var(--imgp-p));' 104 . 'transition:--imgp-p calc(var(--imgp-k,1)*var(--imgp-dc,460ms)*.8) cubic-bezier(.4,0,.6,1),opacity 0s calc(var(--imgp-k,1)*var(--imgp-dc,460ms))}' 105 . '.eimp-ai-badge--floating[aria-expanded="true"],.eimp-ai-badge--floating:not([aria-expanded]):hover,.eimp-ai-badge--floating:not([aria-expanded]):focus{width:var(--imgp-full);transition:width calc(var(--imgp-k,1)*var(--imgp-do,520ms)) var(--imgp-ease-o,cubic-bezier(.2,0,0,1))}' 106 . '.eimp-ai-badge--floating[aria-expanded="true"] .eimp-ai-badge__rest,.eimp-ai-badge--floating:not([aria-expanded]):hover .eimp-ai-badge__rest,.eimp-ai-badge--floating:not([aria-expanded]):focus .eimp-ai-badge__rest{opacity:0;transition:none}' 107 . '.eimp-ai-badge--floating[aria-expanded="true"] .eimp-ai-badge__full,.eimp-ai-badge--floating:not([aria-expanded]):hover .eimp-ai-badge__full,.eimp-ai-badge--floating:not([aria-expanded]):focus .eimp-ai-badge__full{opacity:1;--imgp-p:calc(var(--imgp-full) + var(--imgp-soft,24px));transition:--imgp-p calc(var(--imgp-k,1)*var(--imgp-do,520ms)*var(--imgp-tf-o,.8)) var(--imgp-te-o,cubic-bezier(.25,0,.1,1)) 0s,opacity 0s}' 108 . '@media (prefers-reduced-motion:reduce){.eimp-ai-badge--floating,.eimp-ai-badge--floating>span{transition:none!important}}' 109 // PRO Official Label: large pill by default, small Floating Label 110 // below 500px image width (IMP-316). 111 . '.eimp-ai-badge--floating.eimp-ai-badge--official-small{display:none}' 112 . '@container (max-width:499.98px){.eimp-ai-badge--official-large{display:none}.eimp-ai-badge--floating.eimp-ai-badge--official-small{display:inline-block}}' 113 // Size-dependent labels (IMP-316): on images narrower than 200px 114 // the label is scaled to 70% and sits 4px closer to the corner. 115 // Container query on .eimp-ai-badge-box (the image-sized overlay). 116 . '@container (max-width:199.98px){' 117 . '.eimp-ai-badge--floating,.eimp-ai-badge--circle{transform:scale(.7)}' 118 . '.eimp-ai-badge--pos-top-left{transform-origin:top left;margin:-4px 0 0 -4px}' 119 . '.eimp-ai-badge--pos-top-right{transform-origin:top right;margin:-4px -4px 0 0}' 120 . '.eimp-ai-badge--pos-bottom-left{transform-origin:bottom left;margin:0 0 -4px -4px}' 121 . '.eimp-ai-badge--pos-bottom-right{transform-origin:bottom right;margin:0 -4px -4px 0}' 122 . '}' 85 123 . '</style>'; 124 $this->print_floating_label_js(); 125 } 126 127 /** 128 * Progressive enhancement for the Floating Label (IMP-315). The CSS 129 * hover/focus fallback already works; this adds what CSS can't: 130 * button semantics + aria-expanded, Enter/Space/click toggle, Esc to 131 * dismiss (WCAG 1.4.13: dismissible, hoverable, persistent), tap 132 * outside to close, and no navigation when the label sits inside a link. 133 */ 134 private function print_floating_label_js(): void { 135 $is_pro = (bool) apply_filters( 'eimp_is_pro_active', false ); 136 if ( ! (bool) get_option( 'eimp_ai_label_enabled', 1 ) || ( ! $is_pro && 'floating' !== $this->get_design( false ) ) ) { 137 return; 138 } 139 echo '<script id="eimp-ai-badge-floating-js">' 140 . '(function(){var S=".eimp-ai-badge--floating",t=0;' 141 . 'function o(b){return b.getAttribute("aria-expanded")==="true"}' 142 . 'function set(b,v){b.setAttribute("aria-expanded",v?"true":"false");if(v)t=Date.now()}' 143 . 'function close(b){set(b,false)}' 144 . 'document.querySelectorAll(S).forEach(function(b){' 145 . 'b.setAttribute("role","button");b.setAttribute("aria-expanded","false");' 146 . 'b.addEventListener("pointerenter",function(e){if(e.pointerType!=="touch"&&!b.__d)set(b,true)});' 147 . 'b.addEventListener("pointerleave",function(e){if(e.pointerType==="touch")return;b.__d=false;if(document.activeElement!==b)close(b)});' 148 . 'b.addEventListener("focus",function(){b.__d=false;set(b,true)});' 149 . 'b.addEventListener("blur",function(){b.__d=false;close(b)});' 150 . 'b.addEventListener("click",function(e){if(b.closest("a")){e.preventDefault();e.stopPropagation()}if(Date.now()-t<400)return;set(b,!o(b))});' 151 . 'b.addEventListener("keydown",function(e){if(e.key==="Enter"||e.key===" "){e.preventDefault();set(b,!o(b))}else if(e.key==="Escape"&&o(b)){e.stopPropagation();b.__d=true;close(b)}});' 152 . '});' 153 . 'document.addEventListener("click",function(e){document.querySelectorAll(S+"[aria-expanded=true]").forEach(function(b){if(!b.contains(e.target))close(b)})});' 154 . '})();</script>'; 86 155 } 87 156 … … 311 380 312 381 return (string) preg_replace_callback( 313 '/<div\s[^>]*\bclass="[^"]*\bwp-block-cover__image-background\b[^"]*"[^>]*>\s*<\/div>(?!\s*<span class="eimp-ai-badge ")/i',382 '/<div\s[^>]*\bclass="[^"]*\bwp-block-cover__image-background\b[^"]*"[^>]*>\s*<\/div>(?!\s*<span class="eimp-ai-badge-box")/i', 314 383 function ( array $m ): string { 315 384 if ( ! preg_match( '/\bwp-image-(\d+)\b/', $m[0], $id_match ) ) { … … 326 395 } 327 396 397 /** 398 * Wraps AI-labeled <img> tags with the badge. Images that already sit 399 * directly inside a badge wrapper (e.g. Featured Images, which 400 * inject_thumbnail_badge() wrapped before the page scan runs) are hidden 401 * from the scan, otherwise they get a second wrapper and a second badge: 402 * invisible with the Circle/Official label (identical, stacked), but with 403 * the Floating Label the resting twin shows through the opened one. 404 * 405 * @param string $html Full page HTML. 406 * @return string 407 */ 328 408 private function inject_image_badges( string $html ): string { 409 410 $protected = []; 411 $html = (string) preg_replace_callback( 412 '/(<span class="eimp-ai-badge-wrap[^>]*>)(<img\s[^>]*>)/i', 413 static function ( array $m ) use ( &$protected ): string { 414 $key = '<!--eimp-wrapped-' . count( $protected ) . '-->'; 415 $protected[ $key ] = $m[2]; 416 return $m[1] . $key; 417 }, 418 $html 419 ); 420 421 $html = $this->inject_image_badges_scan( $html ); 422 423 return empty( $protected ) ? $html : str_replace( array_keys( $protected ), array_values( $protected ), $html ); 424 } 425 426 private function inject_image_badges_scan( string $html ): string { 329 427 330 428 $upload_baseurl = wp_get_upload_dir()['baseurl'] ?? ''; … … 507 605 } 508 606 607 // Cover block with the Featured Image as <img>: core CSS pins everything 608 // with .wp-block-cover__image-background absolutely over the whole cover. 609 // A relative wrapper carrying that class (and a copy of the img style) 610 // would sit in normal flow with height 0 and, via overflow:hidden, cut 611 // the photo off – the hero turns empty. Here the wrapper fills the cover 612 // itself (no z-index, so the <img> keeps core's own layering), does not 613 // inherit the cover class, and the badge lifts above the dim overlay 614 // and inner container, same as in the background-image branch. 615 $is_cover_bg = in_array( 'wp-block-cover__image-background', preg_split( '/\s+/', trim( $img_class ) ), true ); 616 if ( $is_cover_bg ) { 617 $wrap_class = 'eimp-ai-badge-wrap' . ( '' !== trim( str_replace( 'wp-block-cover__image-background', '', $img_class ) ) ? ' ' . trim( preg_replace( '/\s+/', ' ', str_replace( 'wp-block-cover__image-background', '', $img_class ) ) ) : '' ); 618 $wrap_style = 'position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;line-height:0;'; 619 return '<span class="' . $wrap_class . '" style="' . $wrap_style . '">' 620 . $img_tag 621 . $this->build_badge_span( $status, 'z-index:2;' ) 622 . '</span>'; 623 } 624 509 625 $wrap_class = 'eimp-ai-badge-wrap' . ( '' !== $img_class ? ' ' . $img_class : '' ); 510 626 // overflow:hidden is the actual guarantee that an oversized custom … … 528 644 529 645 /** 646 * Effective label design: 'circle' (Free default / PRO official pill) or 647 * 'floating'. PRO can steer it via the eimp_ai_badge_design filter. 648 * 649 * @param bool $is_pro Whether PRO is active. 650 * @return string 651 */ 652 private function get_design( bool $is_pro ): string { 653 $design = (string) get_option( 'eimp_ai_label_design', 'circle' ); 654 $design = (string) apply_filters( 'eimp_ai_badge_design', $design, $is_pro ); 655 return 'floating' === $design ? 'floating' : 'circle'; 656 } 657 658 /** 530 659 * Build the visible badge element itself (label, position, icon incl. 531 660 * PRO filters), shared by the <img> wrapper and the Cover-block pass. … … 542 671 543 672 $is_pro = (bool) apply_filters( 'eimp_is_pro_active', false ); 544 $position = $is_pro ? (string) get_option( 'eimp_ai_label_position', 'bottom-right' ) : 'top-left'; 673 $design = $this->get_design( $is_pro ); 674 // Free offers two corners (IMP-315); the other two stay PRO. A stored 675 // PRO-only corner (e.g. after a licence lapse) falls back to top-left. 676 $position = $is_pro 677 ? (string) get_option( 'eimp_ai_label_position', 'bottom-right' ) 678 : (string) get_option( 'eimp_ai_label_position', 'top-left' ); 679 if ( ! $is_pro && ! in_array( $position, [ 'top-left', 'bottom-right' ], true ) ) { 680 $position = 'top-left'; 681 } 545 682 $color = $is_pro ? (string) get_option( 'eimp_ai_label_color', 'white' ) : 'white'; 546 683 $contrast = $is_pro ? (string) get_option( 'eimp_ai_label_contrast', 'normal' ) : 'normal'; … … 560 697 $offset = apply_filters( 'eimp_ai_badge_offset', $offset, $status, $position, $is_pro ); 561 698 562 $icon = $is_pro 563 ? $this->render_pill_icon( $status, $color, $contrast ) 564 : $this->render_circle_icon(); 699 $official_pill = null; 700 if ( 'floating' === $design ) { 701 $icon = $this->render_floating_content( $status, $color, $contrast ); 702 } elseif ( $is_pro ) { 703 $icon = $this->render_pill_icon( $status, $color, $contrast ); 704 $official_pill = $icon; 705 } else { 706 $icon = $this->render_circle_icon(); 707 } 565 708 566 709 // Lets PRO substitute a user-uploaded custom label image for the … … 572 715 // width axis, and to shrink the visible/clipped-to portion 573 716 // gracefully rather than always clipping at an arbitrary point. 574 return '<span class="eimp-ai-badge" role="img" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;max-height:100%;pointer-events:none;line-height:0;' . $extra_style . '">' 717 // Overlay that fills the image/cover and acts as the CSS container 718 // for the size-dependent rules (IMP-316). Absolutely positioned, so 719 // size containment can't collapse the shrink-to-fit wrapper. 720 // container-type makes the box its own stacking context, so a z-index the 721 // caller wants (Cover block: above dim overlay + inner container) has to 722 // sit on the box itself – on the pill it would only act inside the box. 723 $box_z = preg_match( '/z-index:\s*(\d+)\s*;/', $extra_style, $zm ) ? 'z-index:' . (int) $zm[1] . ';' : ''; 724 $box_open = '<span class="eimp-ai-badge-box" style="position:absolute;top:0;right:0;bottom:0;left:0;container-type:inline-size;pointer-events:none;' . $box_z . '">'; 725 $box_close = '</span>'; 726 $pos_class = 'eimp-ai-badge--pos-' . ( isset( $position_css[ $position ] ) ? $position : 'top-left' ); 727 728 if ( 'floating' === $design ) { 729 // Focusable + hover-able, so the CSS-only expand works for mouse, 730 // keyboard and tap even without JS. The inline script in the 731 // footer upgrades it (button role, Enter/Space/Esc, aria-expanded). 732 $extra_style .= 'pointer-events:auto;' . $this->floating_vars( $status, $color, $contrast ); 733 return $box_open . '<span class="eimp-ai-badge eimp-ai-badge--floating ' . $pos_class . '" role="img" tabindex="0" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;' . $extra_style . '">' 734 . $icon 735 . '</span>' . $box_close; 736 } 737 738 // PRO Official Label (unchanged built-in pill): on images narrower 739 // than 500px swap to the small Floating Label instead (IMP-316). Both 740 // variants are rendered; a container query shows only one, and the 741 // hidden one is display:none (not in the accessibility tree). 742 if ( null !== $official_pill && $icon === $official_pill ) { 743 $small_style = 'position:absolute;' . $offset . 'max-width:100%;pointer-events:auto;' . $this->floating_vars( $status, $color, $contrast ); 744 return $box_open 745 . '<span class="eimp-ai-badge eimp-ai-badge--official-large" role="img" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;max-height:100%;pointer-events:none;line-height:0;' . $extra_style . '">' 746 . $icon 747 . '</span>' 748 . '<span class="eimp-ai-badge eimp-ai-badge--floating eimp-ai-badge--official-small ' . $pos_class . '" role="img" tabindex="0" aria-label="' . esc_attr( $label ) . '" style="' . $small_style . '">' 749 . $this->render_floating_content( $status, $color, $contrast ) 750 . '</span>' 751 . $box_close; 752 } 753 754 $circle_class = ( ! $is_pro && 'circle' === $design ) ? ' eimp-ai-badge--circle ' . $pos_class : ''; 755 756 return $box_open . '<span class="eimp-ai-badge' . $circle_class . '" role="img" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;max-height:100%;pointer-events:none;line-height:0;' . $extra_style . '">' 575 757 . $icon 576 . '</span>'; 758 . '</span>' . $box_close; 759 } 760 761 /** 762 * Inner markup of the Floating Label: "AI" at rest, expands to 763 * "AI generated" / "AI modified" on hover/focus (CSS only, see 764 * print_gallery_crop_fix_css()). English only by design (IMP-315). 765 * 766 * @param string $status AiLabelClassifier::STATUS_AI_GENERATED or STATUS_AI_MODIFIED. 767 * @return string 768 */ 769 private function render_floating_content( string $status, string $color = 'white', string $contrast = 'normal' ): string { 770 return '<span class="eimp-ai-badge__rest">' . $this->render_floating_rest_icon( $color, $contrast, false ) . '</span>' 771 . '<span class="eimp-ai-badge__full">' . $this->render_pill_icon( $status, $color, $contrast, false ) . '</span>'; 772 } 773 774 /** 775 * Inline CSS variables for the Floating Label transition: full width 776 * (SVG width + 1.5px offset) and the pill colour, which the badge itself 777 * paints (the SVGs are rendered without their own pill). 778 */ 779 private function floating_vars( string $status, string $color, string $contrast ): string { 780 $full = ( AiLabelClassifier::STATUS_AI_GENERATED === $status ? 104 : 92.4 ) + 1.5; 781 $rgb = 'black' === $color ? '0,0,0' : '255,255,255'; 782 $bg = 'transparent' === $contrast ? 'rgba(' . $rgb . ',.5)' : 'rgb(' . $rgb . ')'; 783 return '--imgp-full:' . $full . 'px;--imgp-bg:' . $bg . ';'; 784 } 785 786 /** 787 * Resting state of the Floating Label: small 35x20 pill with the "AI" 788 * glyph, traced from the Figma "Ai-Label / white subtle" variant. Public 789 * so PRO's settings preview can reuse it. Color/contrast work like the 790 * regular pill (PRO only; Free always passes white/normal). 791 * 792 * @param string $color 'white' or 'black'. 793 * @param string $contrast 'normal' or 'transparent'. 794 * @return string SVG markup. 795 */ 796 public function render_floating_rest_icon( string $color, string $contrast, bool $with_bg = true ): string { 797 $bg_fill = 'black' === $color ? '#000000' : '#ffffff'; 798 $glyph_fill = 'black' === $color ? '#ffffff' : '#000000'; 799 $bg_opacity = 'transparent' === $contrast ? '0.5' : '1'; 800 return '<svg width="35" height="20" viewBox="0 0 35 20" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">' 801 . ( $with_bg ? '<path d="M0 10C0 4.47715 4.47715 0 10 0H25C30.5228 0 35 4.47715 35 10V10C35 15.5228 30.5228 20 25 20H10C4.47715 20 0 15.5228 0 10V10Z" fill="' . esc_attr( $bg_fill ) . '" fill-opacity="' . esc_attr( $bg_opacity ) . '"/>' : '' ) 802 . '<path d="M15.735 5.03442C15.8908 5.03447 16.0124 5.07277 16.1002 5.15063C16.1873 5.2287 16.2453 5.31958 16.273 5.42017L19.5006 14.2756C19.5185 14.3123 19.5279 14.354 19.5279 14.3997C19.5278 14.473 19.4981 14.5397 19.4391 14.5989C19.3791 14.6588 19.3076 14.6877 19.2252 14.6877H17.5279C17.3899 14.6877 17.2866 14.6533 17.2184 14.5842C17.1502 14.5153 17.1053 14.4534 17.0865 14.3987L16.5484 12.9915H12.8658L12.3414 14.3987C12.3226 14.4534 12.281 14.5153 12.2174 14.5842C12.1528 14.6533 12.0425 14.6877 11.8863 14.6877H10.1891C10.1156 14.6877 10.0491 14.658 9.98984 14.5989C9.92992 14.5389 9.90012 14.473 9.9 14.3997C9.9 14.354 9.90471 14.3123 9.91367 14.2756L13.1549 5.42017C13.1827 5.31883 13.2399 5.2287 13.3277 5.15063C13.4148 5.07264 13.5371 5.03442 13.6939 5.03442H15.735ZM22.8092 5.03149C22.9105 5.0315 22.9936 5.06362 23.0582 5.12817C23.1228 5.19273 23.1549 5.27587 23.1549 5.3772V14.343C23.1548 14.4441 23.1226 14.5266 23.0582 14.5911C22.9936 14.6556 22.9105 14.6877 22.8092 14.6877H20.9615C20.8602 14.6877 20.7771 14.6556 20.7125 14.5911C20.6481 14.5266 20.6159 14.4441 20.6158 14.343V5.3772C20.6158 5.27589 20.648 5.19272 20.7125 5.12817C20.7771 5.06362 20.8602 5.03149 20.9615 5.03149H22.8092ZM13.4312 11.0051H15.9967L14.6998 7.36353L13.4312 11.0051Z" fill="' . esc_attr( $glyph_fill ) . '"/>' 803 . '</svg>'; 577 804 } 578 805 … … 583 810 */ 584 811 private function render_circle_icon(): string { 585 return '<svg width=" 50" height="50" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">'812 return '<svg width="30" height="30" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">' 586 813 . '<path fill-rule="evenodd" clip-rule="evenodd" d="M25.0007 0C38.8068 0 50 11.1932 50 25.0007C50 38.8082 38.8068 50 25.0007 50C11.1946 50 0 38.8068 0 25.0007C0 11.1946 11.1932 0 25.0007 0Z" fill="#ffffff"/>' 587 814 . '<path d="M22.229 15.3242C22.5408 15.3243 22.7844 15.4027 22.9595 15.5586C23.133 15.7145 23.2481 15.8934 23.3042 16.0957L29.7524 33.7871C29.7894 33.861 29.8081 33.9435 29.8081 34.0352C29.808 34.1828 29.7473 34.3157 29.6284 34.4346C29.5095 34.5535 29.3671 34.6133 29.2017 34.6133H25.812C25.5372 34.6133 25.3301 34.5453 25.1919 34.4072C25.0538 34.2691 24.9658 34.1459 24.9302 34.0352L23.855 31.2236H16.4985L15.4517 34.0352C15.4147 34.1459 15.3307 34.2692 15.2036 34.4072C15.075 34.5451 14.855 34.6132 14.5435 34.6133H11.1509C11.0033 34.6133 10.8704 34.5547 10.7515 34.4346C10.6312 34.3157 10.5719 34.1828 10.5718 34.0352C10.5718 33.9435 10.5813 33.861 10.5991 33.7871L17.0757 16.0957C17.1304 15.8933 17.2454 15.7145 17.4204 15.5586C17.5941 15.4026 17.8376 15.3242 18.1509 15.3242H22.229ZM36.3647 15.3232C36.5669 15.3233 36.731 15.3873 36.8608 15.5156C36.9894 15.6442 37.0542 15.8102 37.0542 16.0127V33.9238C37.0542 34.1263 36.9894 34.2923 36.8608 34.4209C36.7323 34.5493 36.567 34.6132 36.3647 34.6133H32.6724C32.4699 34.6133 32.3039 34.5495 32.1753 34.4209C32.0468 34.2923 31.9829 34.1262 31.9829 33.9238V16.0127C31.9829 15.8102 32.0467 15.6456 32.1753 15.5156C32.3039 15.3857 32.4699 15.3232 32.6724 15.3232H36.3647ZM17.6284 27.2559H22.7524L20.1626 19.9814L17.6284 27.2559Z" fill="#000000"/>' … … 605 832 * @return string SVG markup. 606 833 */ 607 public function render_pill_icon( string $status, string $color, string $contrast ): string {834 public function render_pill_icon( string $status, string $color, string $contrast, bool $with_bg = true ): string { 608 835 609 836 $generated = AiLabelClassifier::STATUS_AI_GENERATED === $status; … … 623 850 624 851 return '<svg width="' . esc_attr( $width ) . '" height="20" viewBox="0 0 ' . esc_attr( $width ) . ' 20" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">' 625 . '<path d="' . $bg_path . '" fill="' . esc_attr( $bg_fill ) . '" fill-opacity="' . esc_attr( $bg_opacity ) . '"/>'852 . ( $with_bg ? '<path d="' . $bg_path . '" fill="' . esc_attr( $bg_fill ) . '" fill-opacity="' . esc_attr( $bg_opacity ) . '"/>' : '' ) 626 853 . '<path d="' . $text_path . '" fill="' . esc_attr( $text_fill ) . '"/>' 627 854 . '</svg>'; -
enfants-img-performer/trunk/readme.txt
r3786650 r3798740 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.3.1 57 Stable tag: 0.3.16 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 238 238 == Changelog == 239 239 240 = 0.3.16 = 241 * New: Floating Label as a second design for the visible AI label. At rest it is a small "AI" pill; on hover, focus or tap it grows smoothly into "AI generated" or "AI modified". Works with mouse, keyboard (Enter, Space, Esc) and touch, exposes aria-expanded to screen readers, and skips the animation when "reduce motion" is set. 242 * New: Choose the label design (Circle or Floating) and its position (top left or bottom right) under "Visible label on the frontend". 243 * New: Labels adapt to the image width. On images narrower than 200 px the label is smaller and sits closer to the corner. 244 * Changed: The Circle label is slightly smaller. 245 * Fixed: Featured images received two stacked label layers. With the Circle label this went unnoticed, with the Floating Label the second layer showed through. 246 * Fixed: A featured image inside a Cover block (hero) was cut off when it carried the AI label, leaving the hero empty. The image fills the Cover again, and the label sits above the Cover's dim overlay, also for Covers with a fixed background. 247 240 248 = 0.3.15 = 241 249 * Improved: Faster front-end on sites with large media libraries. The plugin no longer looks up the same image in the database over and over on every page view. It now remembers the result (up to 7 days) and clears this memory automatically whenever an image is uploaded, edited, replaced or deleted. On a large test site this cut the plugin's database time on the home page from about 0.7 s to almost zero.
Note:
See TracChangeset
for help on using the changeset viewer.