Changeset 3739629
- Timestamp:
- 10/11/2026 02:52:48 PM (5 weeks ago)
- Location:
- veloimg
- Files:
-
- 10 added
- 26 edited
-
assets/screenshot-1-ru.png (added)
-
assets/screenshot-1.png (modified) ( previous)
-
assets/screenshot-2-ru.png (added)
-
assets/screenshot-2.png (modified) ( previous)
-
assets/screenshot-3-ru.png (added)
-
assets/screenshot-3.png (modified) ( previous)
-
assets/screenshot-4-ru.png (added)
-
assets/screenshot-4.png (modified) ( previous)
-
assets/screenshot-5-ru.png (added)
-
assets/screenshot-5.png (modified) ( previous)
-
trunk/assets/css/admin-base.css (modified) (61 diffs)
-
trunk/assets/js/admin-avif.js (modified) (29 diffs)
-
trunk/assets/js/admin-navigation.js (added)
-
trunk/assets/js/admin.js (modified) (41 diffs)
-
trunk/assets/js/folder-statistics-runtime.js (modified) (2 diffs)
-
trunk/languages/veloimg-ru_RU.mo (modified) ( previous)
-
trunk/languages/veloimg-ru_RU.po (modified) (2 diffs)
-
trunk/readme.txt (modified) (6 diffs)
-
trunk/src/Admin/OptimizationSummary.php (modified) (4 diffs)
-
trunk/src/Admin/SettingsPage.php (modified) (35 diffs)
-
trunk/src/Conversion/UploadsPhysicalScopeSummary.php (modified) (7 diffs)
-
trunk/src/Conversion/WebpManifestMassOrchestrator.php (modified) (2 diffs)
-
trunk/src/Conversion/WebpRetry.php (added)
-
trunk/src/Database/WebpRunStatistics.php (modified) (2 diffs)
-
trunk/src/Delivery/FormatStrategy.php (modified) (1 diff)
-
trunk/src/Delivery/Resolver.php (modified) (6 diffs)
-
trunk/src/Lifecycle/Activate.php (modified) (1 diff)
-
trunk/src/Queue/Worker.php (modified) (3 diffs)
-
trunk/templates/admin-avif.php (modified) (12 diffs)
-
trunk/templates/admin-dashboard.php (modified) (13 diffs)
-
trunk/templates/admin-diagnostics.php (added)
-
trunk/templates/admin-home.php (modified) (4 diffs)
-
trunk/templates/generation-concurrency.php (added)
-
trunk/templates/optimization-home-result.php (modified) (1 diff)
-
trunk/templates/webp-retry.php (added)
-
trunk/veloimg.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
veloimg/trunk/assets/css/admin-base.css
r3728637 r3739629 1 1 /* 2 * VeloIMG Admin UI - WordPress style base skin.2 * VeloIMG Admin UI - approved warm WordPress-native skin. 3 3 * This is the single admin UI stylesheet used by the plugin. 4 4 */ 5 5 6 6 .wrap.velowp{ 7 --vw-border: #c3c4c7; 8 --vw-border-soft: #dcdcde; 9 --vw-surface: #fff; 10 --vw-surface-muted: #f6f7f7; 11 --vw-text: #1d2327; 12 --vw-muted: #646970; 7 --vw-page: #f3f0eb; 8 --vw-border: #ded8d0; 9 --vw-border-soft: #e6e0d8; 10 --vw-control-border: #aaa39a; 11 --vw-surface: #fffdfa; 12 --vw-surface-muted: #f7f4ef; 13 --vw-accent: #a65338; 14 --vw-accent-bg: #f5efe8; 15 --vw-accent-bg-open: #eee5dc; 16 --vw-text: #302d29; 17 --vw-muted: #615950; 13 18 --vw-primary: #2271b1; 14 19 --vw-primary-dark: #135e96; 15 --vw-success: #00a32a; 16 --vw-success-bg: #edfaef; 17 --vw-warning: #dba617; 18 --vw-warning-bg: #fcf9e8; 19 --vw-danger: #d63638; 20 --vw-danger-bg: #fcf0f1; 21 --vw-info-bg: #edf6fc; 20 --vw-success: #309454; 21 --vw-success-text: #206344; 22 --vw-success-border: #c3dacb; 23 --vw-success-bg: #edf5ef; 24 --vw-warning: #b77a1c; 25 --vw-warning-text: #755016; 26 --vw-warning-border: #e6d4af; 27 --vw-warning-bg: #fcf3de; 28 --vw-danger: #b53f42; 29 --vw-danger-text: #853638; 30 --vw-danger-border: #e9c7c8; 31 --vw-danger-bg: #fbefef; 32 --vw-info-border: #cadce7; 33 --vw-info-bg: #edf4f9; 34 --vw-radius: 4px; 35 --vw-panel-radius: 6px; 22 36 color: var(--vw-text); 37 font-size: 14px; 38 line-height: 1.6; 23 39 max-width: 1280px; 40 } 41 42 .wrap.velowp p, 43 .wrap.velowp .description{ 44 font-size: 14px; 45 line-height: 1.6; 24 46 } 25 47 … … 34 56 } 35 57 58 .velowp-concurrency-warning{ 59 position: relative; 60 margin: 12px 0; 61 padding: 12px 44px 12px 16px; 62 border-radius: var(--vw-radius); 63 background: var(--vw-warning-bg); 64 } 65 .velowp-concurrency-dismiss{ 66 position: absolute; 67 top: 6px; 68 right: 6px; 69 width: 32px; 70 height: 32px; 71 padding: 6px; 72 border: 0; 73 border-radius: var(--vw-radius); 74 background: transparent; 75 color: var(--vw-warning-text); 76 cursor: pointer; 77 } 78 .velowp-concurrency-dismiss:hover{ 79 background: var(--vw-warning-border); 80 } 81 .velowp-concurrency-dismiss:focus-visible{ 82 outline: 2px solid var(--vw-primary); 83 outline-offset: 1px; 84 } 85 .velowp-concurrency-warning[hidden]{ 86 display: none; 87 } 88 89 /* WordPress marks JS-enabled pages before footer scripts load. Keep the 90 server-selected panel stable on the first paint; no-JS retains all sections. */ 91 body.js .velowp-shell:not(.velowp-shell--js) .velowp-panel, 36 92 .velowp-shell--js .velowp-panel{ 37 93 display: none; 38 94 } 39 95 96 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="mass"] .velowp-panel[data-panel="mass"], 97 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="setup"] .velowp-panel[data-panel="setup"], 98 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="mass"] .velowp-panel[data-panel="mass"], 99 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="setup"] .velowp-panel[data-panel="setup"], 40 100 .velowp-shell--js .velowp-panel.is-active{ 41 101 display: block; … … 47 107 justify-content: space-between; 48 108 gap: 16px; 49 margin: 16px 0 0; 50 padding: 0 0 14px; 51 border-bottom: 1px solid var(--vw-border); 109 margin: 24px 0 0; 52 110 } 53 111 … … 72 130 .wrap.velowp img.velowp-topbar__logo--wide, 73 131 .velowp-topbar__logo--wide{ 74 width: min(1 10px, 45vw);132 width: min(148px, 45vw); 75 133 height: auto; 76 max-width: min(1 10px, 45vw);77 max-height: 30px;134 max-width: min(148px, 45vw); 135 max-height: 40px; 78 136 } 79 137 … … 83 141 justify-content: space-between; 84 142 gap: 24px; 85 max-width: 1120px;86 143 margin: 24px 0; 87 144 } … … 99 156 100 157 .velowp-home-logo__image--wide{ 101 max-width: 160px;158 max-width: 220px; 102 159 } 103 160 … … 124 181 padding: 3px 8px; 125 182 border: 1px solid var(--vw-border-soft); 183 border-radius: var(--vw-radius); 126 184 background: var(--vw-surface-muted); 127 185 color: var(--vw-text); 128 186 font-size: 12px; 129 187 font-weight: 600; 130 line-height: 1. 7;188 line-height: 1.6; 131 189 } 132 190 133 191 .velowp-pill--ok{ 134 border-color: #b8e6c1;192 border-color: var(--vw-success-border); 135 193 background: var(--vw-success-bg); 136 color: #0a4b1f;194 color: var(--vw-success-text); 137 195 } 138 196 139 197 .velowp-pill--bad{ 140 border-color: #f0c5c7;198 border-color: var(--vw-danger-border); 141 199 background: var(--vw-danger-bg); 142 color: #8a2424;200 color: var(--vw-danger-text); 143 201 } 144 202 … … 147 205 flex-wrap: wrap; 148 206 gap: 0; 149 margin: 18px 0 0; 150 padding-top: 9px; 207 margin: 24px 0 0; 151 208 border-bottom: 1px solid var(--vw-border); 152 209 } … … 156 213 appearance: none; 157 214 border: 1px solid var(--vw-border); 158 background: #f0f0f1;159 color: #2c3338;215 background: var(--vw-accent-bg); 216 color: var(--vw-text); 160 217 cursor: pointer; 161 218 font-size: 14px; 219 font-family: inherit; 162 220 line-height: 1.71428571; 163 221 text-decoration: none; … … 166 224 .velowp-nav__btn{ 167 225 margin: 0 4px -1px 0; 168 padding: 5px 10px; 226 padding: 8px 14px; 227 min-height: 40px; 228 border-radius: var(--vw-radius) var(--vw-radius) 0 0; 169 229 border-bottom: 1px solid var(--vw-border); 170 230 } 171 231 232 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="mass"] .velowp-nav__btn[data-panel="mass"], 233 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="setup"] .velowp-nav__btn[data-panel="setup"], 234 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="mass"] .velowp-nav__btn[data-panel="mass"], 235 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="setup"] .velowp-nav__btn[data-panel="setup"], 172 236 .velowp-nav__btn.is-active{ 173 237 border-bottom-color: var(--vw-surface); 174 238 background: var(--vw-surface); 175 color: #000; 239 color: var(--vw-primary); 240 font-weight: 600; 241 box-shadow: inset 0 2px 0 var(--vw-primary); 242 } 243 244 .velowp-nav__btn:hover, 245 .velowp-topbtn:hover{ 246 color: var(--vw-primary-dark); 247 } 248 249 .velowp-nav__btn:focus-visible, 250 .velowp-topbtn:focus-visible{ 251 outline: 2px solid var(--vw-primary); 252 outline-offset: 2px; 176 253 } 177 254 178 255 .velowp-topbtn{ 179 padding: 4px 10px; 256 padding: 6px 12px; 257 border-radius: var(--vw-radius); 180 258 } 181 259 … … 183 261 .velowp-quick-card, 184 262 .velowp-mass-card{ 263 min-width: 0; 185 264 margin: 16px 0 0; 186 padding: 16px;265 padding: 24px; 187 266 border: 1px solid var(--vw-border); 267 border-radius: var(--vw-panel-radius); 188 268 background: var(--vw-surface); 189 box-shadow: 0 1px 1px rgba(0,0,0,.04);269 box-shadow: none; 190 270 } 191 271 192 272 .velowp-card--glass{ 193 273 background: var(--vw-surface); 274 } 275 276 .velowp-home-accordion{ 277 padding: 0; 278 } 279 280 .velowp-home-accordion > summary{ 281 display: flex; 282 align-items: center; 283 justify-content: space-between; 284 gap: 12px; 285 padding: 16px 20px; 286 cursor: pointer; 287 list-style: none; 288 } 289 290 .velowp-home-accordion > summary:hover{ 291 background: var(--vw-surface-muted); 292 } 293 294 .velowp-home-accordion[open] > summary{ 295 border-bottom: 1px solid var(--vw-border-soft); 296 } 297 298 .velowp-home-accordion > summary::-webkit-details-marker{ 299 display: none; 300 } 301 302 .velowp-home-accordion > summary .velowp-quick-card__title{ 303 margin: 0; 304 } 305 306 .velowp-home-accordion > summary::after{ 307 content: ''; 308 flex: 0 0 8px; 309 height: 8px; 310 border-right: 2px solid currentColor; 311 border-bottom: 2px solid currentColor; 312 transform: rotate(45deg); 313 } 314 315 .velowp-home-accordion[open] > summary::after{ 316 transform: rotate(225deg); 317 } 318 319 .velowp-home-accordion > summary:focus-visible{ 320 outline: 2px solid var(--vw-primary); 321 outline-offset: -2px; 322 } 323 324 .velowp-home-accordion__body{ 325 padding: 20px; 194 326 } 195 327 … … 204 336 205 337 .velowp-mass-grid--top{ 206 grid-template-columns: minmax(0, 1.35fr) minmax(280px, .65fr); 338 grid-template-columns: minmax(0, 1.8fr) minmax(280px, 1fr); 339 } 340 341 .velowp-panel[data-panel="setup"] > .velowp-grid{ 342 grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr); 207 343 } 208 344 209 345 .velowp-mass-grid--bottom{ 210 346 grid-template-columns: 1fr; 347 } 348 349 .velowp-mass-main-column{ 350 min-width: 0; 351 } 352 353 .velowp-webp-retry-controls{ 354 display: flex; 355 align-items: flex-end; 356 gap: 16px; 357 flex-wrap: wrap; 358 } 359 360 .velowp-webp-retry-controls label{ 361 display: block; 362 margin-bottom: 8px; 363 font-weight: 600; 364 } 365 366 .velowp-webp-retry-quality{ 367 flex: 1 1 200px; 368 } 369 370 .velowp-webp-retry-quality input{ 371 width: 100%; 372 margin: 0; 373 } 374 375 .velowp-webp-retry-engine select{ 376 min-width: 140px; 377 max-width: 100%; 211 378 } 212 379 … … 220 387 221 388 .velowp-h2{ 222 font-size: 2 0px;389 font-size: 22px; 223 390 font-weight: 600; 224 391 } … … 227 394 .velowp-h3, 228 395 .velowp-quick-card__title{ 229 font-size: 1 6px;396 font-size: 18px; 230 397 font-weight: 600; 231 398 } … … 256 423 .wrap.velowp input[type="url"]{ 257 424 max-width: 100%; 425 min-height: 38px; 426 padding: 6px 10px; 427 border-color: var(--vw-control-border); 428 border-radius: var(--vw-radius); 429 font-size: 14px; 430 line-height: 1.6; 431 } 432 433 .wrap.velowp select{ 434 padding-right: 30px; 435 } 436 437 .wrap.velowp input[type="range"], 438 .wrap.velowp input[type="checkbox"], 439 .wrap.velowp input[type="radio"]{ 440 accent-color: var(--vw-primary); 441 } 442 443 .wrap.velowp input[type="checkbox"], 444 .wrap.velowp input[type="radio"]{ 445 flex-shrink: 0; 258 446 } 259 447 … … 263 451 264 452 .wrap.velowp .velowp-home-card{ 265 max-width: 1120px; 453 max-width: none; 454 padding: 24px; 455 border-color: var(--vw-border); 456 border-radius: var(--vw-panel-radius); 457 background: var(--vw-surface); 458 box-shadow: none; 459 } 460 461 .velowp-home-card > h2{ 462 margin: 0 0 12px; 463 font-size: 22px; 464 line-height: 1.3; 266 465 } 267 466 268 467 .velowp-home-status-grid{ 269 468 display: grid; 270 grid-template-columns: repeat( auto-fit, minmax(220px, 1fr));271 gap: 1 2px;272 margin: 18px 0;469 grid-template-columns: repeat(4, minmax(0, 1fr)); 470 gap: 16px; 471 margin: 24px 0 20px; 273 472 } 274 473 … … 276 475 min-width: 0; 277 476 min-height: 150px; 278 padding: 1 4px;477 padding: 16px; 279 478 border: 1px solid var(--vw-border); 479 border-radius: var(--vw-radius); 280 480 background: var(--vw-surface); 281 box-shadow: 0 1px 1px rgba(0,0,0,.04); 282 } 283 284 .velowp-home-status-card.is-success{ 285 border-color: #b8e6c1; 286 background: linear-gradient(0deg, var(--vw-success-bg), var(--vw-surface) 58%); 287 } 288 289 .velowp-home-status-card.is-warning{ 290 border-color: #ead483; 291 background: linear-gradient(0deg, var(--vw-warning-bg), var(--vw-surface) 58%); 292 } 293 294 .velowp-home-status-card.is-error{ 295 border-color: #f0c5c7; 296 background: linear-gradient(0deg, var(--vw-danger-bg), var(--vw-surface) 58%); 481 box-shadow: none; 297 482 } 298 483 … … 307 492 308 493 .velowp-home-status-card__head strong{ 309 flex: 1 1 1 20px;494 flex: 1 1 100px; 310 495 min-width: 0; 311 font-size: 1 4px;496 font-size: 16px; 312 497 line-height: 1.35; 313 498 } … … 334 519 335 520 .velowp-home-strategy-value.is-success{ 336 border-color: #b8e6c1;521 border-color: var(--vw-success-border); 337 522 background: var(--vw-success-bg); 338 color: #0a4b1f;523 color: var(--vw-success-text); 339 524 } 340 525 341 526 .velowp-home-strategy-value.is-warning{ 342 border-color: #ead483;527 border-color: var(--vw-warning-border); 343 528 background: var(--vw-warning-bg); 344 color: #5c4300;529 color: var(--vw-warning-text); 345 530 } 346 531 347 532 .velowp-home-strategy-value.is-error{ 348 border-color: #f0c5c7;533 border-color: var(--vw-danger-border); 349 534 background: var(--vw-danger-bg); 350 color: #8a2424; 535 color: var(--vw-danger-text); 536 } 537 538 .wrap.velowp select.velowp-home-badge-select{ 539 appearance: auto; 540 background-image: none; 541 min-height: 38px; 542 max-width: 100%; 543 width: 100%; 544 padding: 6px 24px 6px 10px; 545 border-color: var(--vw-border); 546 border-radius: var(--vw-radius); 547 background-color: var(--vw-surface-muted); 548 color: var(--vw-text); 549 font-size: 14px; 550 font-weight: 400; 551 line-height: 1.6; 552 cursor: pointer; 553 text-align: left; 554 } 555 556 .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head{ 557 flex-direction: column; 558 align-items: stretch; 559 } 560 561 .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head strong{ 562 flex: none; 563 } 564 565 .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head, 566 .velowp-home-status-card--strategy .velowp-home-status-card__head{ 567 min-height: 0; 568 margin-bottom: 10px; 569 } 570 571 .velowp-home-generation-result{ 572 margin-top: 16px; 573 padding-top: 12px; 574 border-top: 1px solid var(--vw-border-soft); 575 } 576 577 .velowp-home-generation-result [data-velowp-home-summary="saved"] strong{ 578 display: block; 579 margin-top: 4px; 580 color: var(--vw-text); 581 font-size: 34px; 582 font-weight: 600; 583 line-height: 1.2; 584 } 585 586 .wrap.velowp select.velowp-home-strategy-value{ 587 width: 100%; 351 588 } 352 589 … … 355 592 } 356 593 594 .velowp-diagnostics-tests{ 595 display: grid; 596 grid-template-columns: repeat(2, minmax(0, 1fr)); 597 gap: 16px; 598 } 599 600 .velowp-diagnostics-tests > form{ 601 min-width: 0; 602 margin-top: 0; 603 padding: 16px; 604 } 605 606 .velowp-bypass-copy{ 607 display: flex; 608 flex-wrap: wrap; 609 align-items: center; 610 gap: 8px; 611 } 612 613 .velowp-bypass-copy input.regular-text{ 614 max-width: 100%; 615 cursor: pointer; 616 } 617 618 @media (max-width: 782px){ 619 .velowp-diagnostics-tests{ grid-template-columns: minmax(0, 1fr); } 620 } 621 357 622 .velowp-help-page{ 358 max-width: 1 120px;623 max-width: 1280px; 359 624 } 360 625 … … 368 633 .velowp-help-grid{ 369 634 display: grid; 370 grid-template-columns: repeat( auto-fit, minmax(220px, 1fr));635 grid-template-columns: repeat(5, minmax(0, 1fr)); 371 636 gap: 12px; 372 margin: 18px 0;637 margin: 24px 0; 373 638 } 374 639 375 640 .velowp-help-card{ 376 padding: 14px; 641 min-width: 0; 642 padding: 16px; 377 643 border: 1px solid var(--vw-border); 644 border-radius: var(--vw-radius); 378 645 background: var(--vw-surface); 379 box-shadow: 0 1px 1px rgba(0,0,0,.04);646 box-shadow: none; 380 647 } 381 648 382 649 .velowp-help-card h2{ 383 650 margin: 0 0 8px; 384 font-size: 1 4px;651 font-size: 16px; 385 652 line-height: 1.35; 386 653 } … … 392 659 393 660 .velowp-help-section{ 394 max-width: 1120px;661 max-width: none; 395 662 } 396 663 … … 403 670 .velowp-help-faq details{ 404 671 border: 1px solid var(--vw-border-soft); 672 border-radius: var(--vw-radius); 405 673 background: var(--vw-surface); 406 674 } 407 675 408 676 .velowp-help-faq summary{ 409 padding: 12px 14px; 677 display: flex; 678 align-items: center; 679 justify-content: space-between; 680 gap: 16px; 681 padding: 12px 16px; 410 682 color: var(--vw-text); 411 683 font-weight: 600; 412 684 cursor: pointer; 685 list-style: none; 686 } 687 688 .velowp-help-faq summary::-webkit-details-marker{ 689 display: none; 690 } 691 692 .velowp-help-faq summary::after{ 693 content: ''; 694 flex: 0 0 8px; 695 height: 8px; 696 border-right: 2px solid currentColor; 697 border-bottom: 2px solid currentColor; 698 transform: rotate(45deg); 699 } 700 701 .velowp-help-faq details[open] summary::after{ 702 color: var(--vw-primary); 703 transform: rotate(225deg); 704 } 705 706 .velowp-help-faq summary:focus-visible{ 707 outline: 2px solid var(--vw-primary); 708 outline-offset: -2px; 413 709 } 414 710 415 711 .velowp-help-faq details[open] summary{ 416 712 border-bottom: 1px solid var(--vw-border-soft); 417 background: var(--vw-surface -muted);713 background: var(--vw-surface); 418 714 } 419 715 420 716 .velowp-help-faq p{ 421 717 margin: 0; 422 padding: 12px 1 4px 14px;718 padding: 12px 16px 16px; 423 719 color: var(--vw-muted); 424 720 } … … 428 724 align-items: center; 429 725 gap: 10px; 430 max-width: 420px;726 width: 100%; 431 727 } 432 728 433 729 .velowp-range input[type="range"]{ 434 flex: 1; 730 flex: 1 1 auto; 731 min-width: 0; 732 margin: 0; 733 height: 28px; 435 734 } 436 735 … … 439 738 text-align: right; 440 739 font-weight: 600; 740 } 741 742 .velowp-range-scale{ 743 display: flex; 744 justify-content: space-between; 745 margin-right: 46px; 746 color: var(--vw-muted); 747 font-size: 12px; 441 748 } 442 749 … … 447 754 align-items: center; 448 755 gap: 8px; 756 } 757 758 .velowp-toggle{ 759 align-items: flex-start; 760 } 761 762 .velowp-toggle input{ 763 margin-top: 3px; 449 764 } 450 765 … … 459 774 border-radius: 50%; 460 775 background: var(--vw-surface-muted); 461 color: #2c3338;776 color: var(--vw-text); 462 777 font-size: 12px; 463 778 font-weight: 600; … … 525 840 } 526 841 842 .velowp-panel[data-panel="setup"] .form-table tr{ 843 display: block; 844 padding: 20px 0; 845 border-top: 1px solid var(--vw-border-soft); 846 } 847 848 .velowp-panel[data-panel="setup"] .form-table th, 849 .velowp-panel[data-panel="setup"] .form-table td{ 850 display: block; 851 width: 100%; 852 padding: 0; 853 } 854 855 .velowp-panel[data-panel="setup"] .form-table th{ 856 margin-bottom: 12px; 857 font-size: 18px; 858 } 859 860 .velowp-verdict strong{ 861 display: inline-block; 862 margin: 0 0 12px; 863 padding: 3px 10px; 864 border: 1px solid var(--vw-border-soft); 865 border-radius: var(--vw-radius); 866 } 867 868 .velowp-verdict-ok strong{ 869 border-color: var(--vw-success-border); 870 background: var(--vw-success-bg); 871 color: var(--vw-success-text); 872 } 873 874 .velowp-verdict-bad strong{ 875 border-color: var(--vw-danger-border); 876 background: var(--vw-danger-bg); 877 color: var(--vw-danger-text); 878 } 879 880 .velowp-verdict .description{ 881 display: block; 882 margin-bottom: 16px; 883 } 884 527 885 .velowp-strategy-options{ 528 886 display: grid; … … 559 917 } 560 918 919 .velowp-diagnostics-tests .velowp-quick-inline--top{ 920 flex-direction: column; 921 align-items: stretch; 922 } 923 924 .velowp-diagnostics-tests .velowp-quick-pick{ 925 flex: 0 1 auto; 926 } 927 561 928 .velowp-quick-pick{ 562 929 flex: 1 1 320px; … … 573 940 .wrap.velowp .velowp-btn-glass--primary{ 574 941 min-width: 0; 942 min-height: 38px; 943 padding: 6px 14px; 944 } 945 946 .wrap.velowp .button{ 947 min-height: 38px; 948 padding: 6px 14px; 949 border-color: var(--vw-primary); 950 border-radius: var(--vw-radius); 951 background: var(--vw-surface); 952 color: var(--vw-primary); 953 font-family: inherit; 954 font-size: 14px; 955 line-height: 1.6; 956 } 957 958 .wrap.velowp .button-primary{ 959 border-color: var(--vw-primary); 960 background: var(--vw-primary); 961 color: #fff; 962 } 963 964 .wrap.velowp .button-primary:hover, 965 .wrap.velowp .button-primary:focus{ 966 border-color: var(--vw-primary-dark); 967 background: var(--vw-primary-dark); 968 } 969 970 .wrap.velowp .button-small{ 575 971 min-height: 30px; 576 padding: 0 12px; 972 padding: 3px 10px; 973 font-size: 12px; 577 974 } 578 975 … … 584 981 .wrap.velowp .velowp-btn-warn{ 585 982 border-color: var(--vw-warning); 586 color: #7a5b00;983 color: var(--vw-warning-text); 587 984 } 588 985 589 986 .wrap.velowp .velowp-btn-danger{ 590 border-color: var(--vw-danger );591 background: var(--vw-danger );592 color: #fff;987 border-color: var(--vw-danger-border); 988 background: var(--vw-danger-bg); 989 color: var(--vw-danger-text); 593 990 } 594 991 595 992 .wrap.velowp .velowp-btn-danger-outline{ 596 993 border-color: var(--vw-danger); 597 color: #8a2424;994 color: var(--vw-danger-text); 598 995 } 599 996 600 997 .wrap.velowp .button[disabled], 601 998 .wrap.velowp .button:disabled{ 602 opacity: .65; 999 border-color: var(--vw-border) !important; 1000 background: var(--vw-surface-muted) !important; 1001 color: #756f65 !important; 1002 opacity: 1; 603 1003 cursor: not-allowed; 604 1004 } … … 650 1050 .velowp-quick-result-row{ 651 1051 display: grid; 652 grid-template-columns: 220pxminmax(0, 1fr);1052 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); 653 1053 gap: 12px; 654 1054 padding-top: 10px; … … 670 1070 align-items: center; 671 1071 gap: 8px; 1072 } 1073 1074 .velowp-quick-result-value{ 1075 min-width: 0; 1076 overflow-wrap: anywhere; 672 1077 } 673 1078 … … 699 1104 padding: 12px; 700 1105 border: 1px solid var(--vw-border-soft); 1106 border-radius: var(--vw-radius); 701 1107 background: var(--vw-surface-muted); 702 1108 } … … 707 1113 708 1114 .velowp-self-test-summary.is-success{ 709 border-left: 4px solid var(--vw-success);710 1115 background: var(--vw-success-bg); 711 1116 } 712 1117 713 1118 .velowp-self-test-summary.is-warning{ 714 border-left: 4px solid var(--vw-warning);715 1119 background: var(--vw-warning-bg); 716 1120 } 717 1121 718 1122 .velowp-self-test-summary.is-error{ 719 border-left: 4px solid var(--vw-danger);720 1123 background: var(--vw-danger-bg); 721 1124 } … … 733 1136 padding: 10px 12px; 734 1137 border: 1px solid var(--vw-border-soft); 1138 border-radius: var(--vw-radius); 735 1139 background: var(--vw-surface); 736 1140 } … … 749 1153 } 750 1154 751 .velowp-home-generation-result{ border-top: 1px solid var(--vw-border, #dcdcde); margin-top: 10px; padding-top: 8px; }752 1155 .velowp-home-last-scope{ display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } 753 1156 … … 758 1161 padding: 2px 8px; 759 1162 border: 1px solid var(--vw-border-soft); 1163 border-radius: var(--vw-radius); 760 1164 background: var(--vw-surface); 761 1165 font-size: 12px; … … 766 1170 .velowp-self-test-badge.is-success, 767 1171 .velowp-badge--success{ 768 border-color: #b8e6c1;1172 border-color: var(--vw-success-border); 769 1173 background: var(--vw-success-bg); 770 color: #0a4b1f;1174 color: var(--vw-success-text); 771 1175 } 772 1176 773 1177 .velowp-self-test-badge.is-warning, 774 1178 .velowp-badge--warning{ 775 border-color: #ead9a1;1179 border-color: var(--vw-warning-border); 776 1180 background: var(--vw-warning-bg); 777 color: #7a5b00;1181 color: var(--vw-warning-text); 778 1182 } 779 1183 780 1184 .velowp-self-test-badge.is-error, 781 1185 .velowp-badge--danger{ 782 border-color: #f0c5c7;1186 border-color: var(--vw-danger-border); 783 1187 background: var(--vw-danger-bg); 784 color: #8a2424;1188 color: var(--vw-danger-text); 785 1189 } 786 1190 787 1191 .velowp-self-test-badge.is-info, 788 1192 .velowp-badge--info{ 789 border-color: #c5d9ed;1193 border-color: var(--vw-info-border); 790 1194 background: var(--vw-info-bg); 791 color: #0a4b78;1195 color: var(--vw-primary-dark); 792 1196 } 793 1197 … … 811 1215 .velowp-audit-metrics{ 812 1216 display: grid; 813 grid-template-columns: repeat( auto-fit, minmax(210px, 1fr));814 gap: 1 0px;1217 grid-template-columns: repeat(4, minmax(0, 1fr)); 1218 gap: 16px; 815 1219 margin-top: 12px; 816 1220 } … … 818 1222 .velowp-audit-metric{ 819 1223 display: grid; 820 grid-template-columns: minmax(0, 1fr) auto;1224 grid-template-columns: minmax(0, 1fr); 821 1225 grid-template-areas: 822 "head value" 823 "description description"; 1226 "head" 1227 "value" 1228 "description"; 824 1229 align-items: flex-start; 825 1230 gap: 8px 14px; 826 1231 min-height: 86px; 827 padding: 12px; 1232 min-width: 0; 1233 padding: 16px; 828 1234 border: 1px solid var(--vw-border-soft); 829 border- left: 4px solid var(--vw-border-soft);1235 border-radius: var(--vw-radius); 830 1236 background: var(--vw-surface); 831 box-shadow: 0 1px 1px rgba(0,0,0,.03); 832 } 833 834 .velowp-audit-metric.is-success{ 835 border-left-color: var(--vw-success); 836 background: linear-gradient(90deg, var(--vw-success-bg), var(--vw-surface) 42%); 837 } 838 839 .velowp-audit-metric.is-warning{ 840 border-left-color: var(--vw-warning); 841 background: linear-gradient(90deg, var(--vw-warning-bg), var(--vw-surface) 42%); 842 } 843 844 .velowp-audit-metric.is-error{ 845 border-left-color: var(--vw-danger); 846 background: linear-gradient(90deg, var(--vw-danger-bg), var(--vw-surface) 42%); 847 } 848 849 .velowp-audit-metric.is-info{ 850 border-left-color: #72aee6; 851 background: linear-gradient(90deg, var(--vw-info-bg), var(--vw-surface) 42%); 1237 box-shadow: none; 1238 } 1239 1240 .velowp-audit-metrics:not(.velowp-audit-metrics--no-generated) > .velowp-audit-metric:nth-child(n+5){ 1241 grid-column: span 2; 852 1242 } 853 1243 854 1244 .velowp-audit-metric__head{ 855 1245 display: flex; 856 flex-wrap: wrap;857 align-items: center;1246 justify-content: space-between; 1247 align-items: flex-start; 858 1248 gap: 6px; 859 1249 grid-area: head; … … 862 1252 863 1253 .velowp-audit-metric span{ 864 color: var(--vw- muted);865 font-size: 1 2px;866 font-weight: 600; 867 text-transform: uppercase;1254 color: var(--vw-text); 1255 font-size: 16px; 1256 font-weight: 600; 1257 line-height: 1.35; 868 1258 } 869 1259 … … 883 1273 884 1274 .velowp-audit-metric.is-success em{ 885 border-color: #b8e6c1;1275 border-color: var(--vw-success-border); 886 1276 background: var(--vw-success-bg); 887 color: #0a4b1f;1277 color: var(--vw-success-text); 888 1278 } 889 1279 890 1280 .velowp-audit-metric.is-warning em{ 891 border-color: #ead9a1;1281 border-color: var(--vw-warning-border); 892 1282 background: var(--vw-warning-bg); 893 color: #7a5b00;1283 color: var(--vw-warning-text); 894 1284 } 895 1285 896 1286 .velowp-audit-metric.is-error em{ 897 border-color: #f0c5c7;1287 border-color: var(--vw-danger-border); 898 1288 background: var(--vw-danger-bg); 899 color: #8a2424;1289 color: var(--vw-danger-text); 900 1290 } 901 1291 902 1292 .velowp-audit-metric.is-info em{ 903 border-color: #c5d9ed;1293 border-color: var(--vw-info-border); 904 1294 background: var(--vw-info-bg); 905 color: #0a4b78;1295 color: var(--vw-primary-dark); 906 1296 } 907 1297 908 1298 .velowp-audit-metric__value{ 909 1299 grid-area: value; 910 justify-self: end;911 color: var(--vw- text);912 font-size: 24px;1300 justify-self: start; 1301 color: var(--vw-primary); 1302 font-size: 34px; 913 1303 line-height: 1.1; 914 1304 white-space: nowrap; … … 916 1306 917 1307 .velowp-audit-metric.is-success .velowp-audit-metric__value{ 918 color: #0a4b1f;1308 color: var(--vw-primary); 919 1309 } 920 1310 921 1311 .velowp-audit-metric.is-warning .velowp-audit-metric__value{ 922 color: #7a5b00;1312 color: var(--vw-warning-text); 923 1313 } 924 1314 925 1315 .velowp-audit-metric.is-error .velowp-audit-metric__value{ 926 color: #8a2424;1316 color: var(--vw-danger-text); 927 1317 } 928 1318 929 1319 .velowp-audit-metric.is-info .velowp-audit-metric__value{ 930 color: #0a4b78;1320 color: var(--vw-primary); 931 1321 } 932 1322 … … 935 1325 grid-area: description; 936 1326 color: var(--vw-muted); 937 font-size: 12px; 938 line-height: 1.35; 1327 padding-top: 10px; 1328 border-top: 1px solid var(--vw-border-soft); 1329 font-size: 14px; 1330 line-height: 1.5; 939 1331 } 940 1332 941 1333 .velowp-audit-metrics--no-generated{ 942 grid-template-columns: repeat( auto-fit, minmax(260px, 1fr));1334 grid-template-columns: repeat(2, minmax(0, 1fr)); 943 1335 } 944 1336 … … 960 1352 padding: 12px 14px; 961 1353 border: 1px solid var(--vw-border-soft); 962 border-left: 4px solid var(--vw-warning);963 1354 background: var(--vw-warning-bg); 964 1355 } … … 1014 1405 align-items: center; 1015 1406 gap: 6px; 1016 padding: 5px 9px;1407 padding: 7px 10px; 1017 1408 border: 1px solid var(--vw-border-soft); 1409 border-radius: var(--vw-radius); 1018 1410 background: var(--vw-surface-muted); 1019 1411 cursor: pointer; 1412 } 1413 1414 .velowp-seg:has(input:checked){ 1415 border-color: #72aee6; 1416 background: var(--vw-info-bg); 1417 } 1418 1419 .velowp-seg:has(input:focus-visible){ 1420 outline: 2px solid var(--vw-primary); 1421 outline-offset: 2px; 1422 } 1423 1424 .velowp-seg input{ 1425 margin: 0; 1020 1426 } 1021 1427 … … 1040 1446 padding: 7px 10px; 1041 1447 border: 1px solid var(--vw-border-soft); 1448 border-radius: var(--vw-radius); 1042 1449 background: var(--vw-surface); 1043 1450 } … … 1053 1460 1054 1461 .velowp-chip--ok{ 1055 border-color: #b8e6c1;1462 border-color: var(--vw-success-border); 1056 1463 background: var(--vw-success-bg); 1057 color: #0a4b1f;1464 color: var(--vw-success-text); 1058 1465 } 1059 1466 1060 1467 .velowp-chip--queue{ 1061 border-color: #c5d9ed;1468 border-color: var(--vw-info-border); 1062 1469 background: var(--vw-info-bg); 1063 color: #0a4b78;1470 color: var(--vw-primary-dark); 1064 1471 } 1065 1472 1066 1473 .velowp-chip--bad{ 1067 border-color: #f0c5c7;1068 background: var(--vw- danger-bg);1069 color: #8a2424;1474 border-color: var(--vw-warning-border); 1475 background: var(--vw-warning-bg); 1476 color: var(--vw-warning-text); 1070 1477 } 1071 1478 … … 1079 1486 .velowp-progress{ 1080 1487 flex: 1 1 auto; 1081 height: 1 2px;1488 height: 14px; 1082 1489 overflow: hidden; 1083 1490 border: 1px solid var(--vw-border-soft); 1491 border-radius: var(--vw-radius); 1084 1492 background: var(--vw-surface-muted); 1085 1493 } … … 1113 1521 .velowp-donut{ 1114 1522 --pct: 0; 1115 width: 1 48px;1116 height: 1 48px;1117 margin: 0 auto 14px;1523 width: 168px; 1524 height: 168px; 1525 margin: 16px auto 24px; 1118 1526 border-radius: 50%; 1119 background: conic-gradient(var(--vw-primary) calc(var(--pct) * 1%), #dcdcde0);1527 background: conic-gradient(var(--vw-primary) calc(var(--pct) * 1%), var(--vw-border) 0); 1120 1528 display: flex; 1121 1529 align-items: center; … … 1124 1532 1125 1533 .velowp-donut__center{ 1126 width: 104px; 1127 height: 104px; 1128 border: 1px solid var(--vw-border-soft); 1534 width: 120px; 1535 height: 120px; 1129 1536 border-radius: 50%; 1130 1537 background: var(--vw-surface); … … 1137 1544 1138 1545 .velowp-donut__pct{ 1139 font-size: 2 4px;1546 font-size: 26px; 1140 1547 font-weight: 600; 1141 1548 } … … 1153 1560 .velowp-bars__row{ 1154 1561 display: grid; 1155 grid-template-columns: 78px minmax(0, 1fr) 90px;1562 grid-template-columns: 78px minmax(0, 1fr) 64px; 1156 1563 gap: 10px; 1157 1564 align-items: center; … … 1193 1600 display: grid; 1194 1601 grid-template-columns: repeat(2, minmax(0, 1fr)); 1195 gap: 1 0px;1602 gap: 16px; 1196 1603 } 1197 1604 … … 1204 1611 } 1205 1612 1206 .velowp-folder-row--active td{ background: #f0f6ff; }1207 .velowp-folder-row--stopped td{ background: #fff8e6; }1208 .velowp-folder-status{ margin-left: 6px; color: #50575e; font-size: 11px; font-weight: 600; }1613 .velowp-folder-row--active td{ background: var(--vw-info-bg); } 1614 .velowp-folder-row--stopped td{ background: var(--vw-surface-muted); } 1615 .velowp-folder-status{ margin-left: 6px; color: var(--vw-muted); font-size: 11px; font-weight: 600; } 1209 1616 .velowp-folder-more-wrap{ margin: 10px 0 0; } 1210 1617 … … 1263 1670 } 1264 1671 1265 #velowp-mass.is-running .velowp-mass-mode, 1266 #velowp-mass.is-running #velowp-mass-folder-field, 1267 #velowp-mass.is-running #velowp-mass-theme-field{ 1672 .velowp-mass.is-running .velowp-mass-mode, 1673 .velowp-mass.is-running .velowp-mass-folder{ 1268 1674 opacity: .7; 1269 1675 } … … 1272 1678 .velowp-table-tight td{ 1273 1679 vertical-align: middle; 1680 padding: 12px; 1681 font-size: 14px; 1682 } 1683 1684 .wrap.velowp .widefat{ 1685 border-color: var(--vw-border-soft); 1686 border-radius: var(--vw-radius); 1687 box-shadow: none; 1688 } 1689 1690 .wrap.velowp .widefat th{ 1691 font-weight: 600; 1692 } 1693 1694 .wrap.velowp .widefat td, 1695 .wrap.velowp .widefat th{ 1696 color: var(--vw-text); 1697 border-bottom: 1px solid var(--vw-border-soft); 1698 } 1699 1700 .wrap.velowp .widefat tr:last-child td, 1701 .wrap.velowp .widefat tr:last-child th{ 1702 border-bottom: 0; 1703 } 1704 1705 .velowp-table-scroll{ 1706 max-width: 100%; 1707 overflow-x: auto; 1708 } 1709 1710 .velowp-table-scroll:focus-visible{ 1711 outline: 2px solid var(--vw-primary); 1712 outline-offset: 2px; 1713 } 1714 1715 .velowp-table-scroll > table{ 1716 min-width: 760px; 1717 } 1718 1719 .velowp-panel[data-panel="setup"] .widefat{ 1720 margin-top: 16px; 1721 } 1722 1723 .velowp-panel[data-panel="setup"] .widefat.striped > tbody > :nth-child(odd){ 1724 background: var(--vw-surface); 1274 1725 } 1275 1726 … … 1293 1744 .velowp-mass-grid, 1294 1745 .velowp-mass-layout, 1295 .velowp-mass-grid--top{ 1746 .velowp-mass-grid--top, 1747 .velowp-panel[data-panel="setup"] > .velowp-grid{ 1296 1748 grid-template-columns: 1fr; 1749 } 1750 .velowp-home-status-grid, 1751 .velowp-help-grid, 1752 .velowp-audit-metrics{ 1753 grid-template-columns: repeat(2, minmax(0, 1fr)); 1754 } 1755 .velowp-audit-metrics:not(.velowp-audit-metrics--no-generated) > .velowp-audit-metric:nth-child(n+5){ 1756 grid-column: auto; 1297 1757 } 1298 1758 } … … 1317 1777 } 1318 1778 1319 .velowp-quick-result-row, 1320 .velowp-bars__row, 1321 .velowp-summarybox__grid{ 1779 .velowp-quick-result-row{ 1322 1780 grid-template-columns: 1fr; 1323 1781 } 1324 1782 1325 .wrap.velowp .velowp-btn-wide{ 1326 width: auto; 1783 .velowp-card, 1784 .velowp-quick-card, 1785 .velowp-mass-card, 1786 .wrap.velowp .velowp-home-card{ 1787 padding: 16px; 1327 1788 } 1328 } 1329 .wrap.velowp .velowp-mass-card--summary.velowp-statistics-frozen > :not(h3):not([role="status"]){ 1330 opacity: .6; 1331 } 1332 .velowp-home-generation-result[data-velowp-home-frozen="1"] [data-velowp-home-summary-group]{ 1789 .velowp-home-accordion{ 1790 padding: 0; 1791 } 1792 .velowp-home-accordion > summary, 1793 .velowp-home-accordion__body{ 1794 padding: 16px; 1795 } 1796 .velowp-home-logo__image--wide{ max-width: 180px; } 1797 .velowp-chip{ padding: 6px 8px; } 1798 .velowp-chip__icon{ width: 16px; } 1799 .velowp-home-status-card__head strong{ flex-basis: 120px; } 1800 } 1801 1802 @media (max-width: 600px){ 1803 .velowp-home-status-grid, 1804 .velowp-help-grid, 1805 .velowp-audit-metrics, 1806 .velowp-audit-metrics--no-generated{ 1807 grid-template-columns: minmax(0, 1fr); 1808 } 1809 .velowp-home-status-card{ min-height: 0; } 1810 .velowp-self-test-item{ flex-wrap: wrap; } 1811 .velowp-bars__row{ grid-template-columns: 76px minmax(0, 1fr) 60px; gap: 8px; } 1812 } 1813 .wrap.velowp .velowp-mass-card--summary.velowp-statistics-frozen .velowp-optimization-totals > :not(.description), 1814 .wrap.velowp .velowp-home-generation-result[data-velowp-home-frozen="1"] [data-velowp-home-summary-group]{ 1815 /* Dim saved figures only; the freshness notice and file actions remain clear. */ 1333 1816 opacity: .6; 1334 1817 } … … 1358 1841 filter: none; 1359 1842 } 1843 /* Internal navigation preserves the WordPress shell and the current confirmed view. */ 1844 .velowp.velowp-navigation-loading { cursor: progress; } 1845 .velowp.velowp-navigation-loading > :not(#velowp-navigation-status) { pointer-events: none; opacity: .65; } 1846 .velowp #velowp-navigation-status { margin: 0 0 16px; } 1847 .velowp #velowp-navigation-status[role="status"] { 1848 position: fixed; 1849 right: 20px; 1850 bottom: 20px; 1851 z-index: 100000; 1852 max-width: calc(100vw - 40px); 1853 box-sizing: border-box; 1854 margin: 0; 1855 pointer-events: none; 1856 } 1857 1858 /* One visual language for Home, WebP, AVIF and Help; no controller-specific skin. */ 1859 body.wp-admin:has(.wrap.velowp){ 1860 background: #f3f0eb; 1861 } 1862 .wrap.velowp{ 1863 font-variant-numeric: tabular-nums; 1864 } 1865 .wrap.velowp a{ 1866 color: var(--vw-primary); 1867 } 1868 .wrap.velowp a:hover{ 1869 color: var(--vw-primary-dark); 1870 } 1871 .wrap.velowp .button-primary, 1872 .wrap.velowp .button-primary:hover{ 1873 color: #fff; 1874 } 1875 .wrap.velowp input[type="text"], 1876 .wrap.velowp input[type="url"], 1877 .wrap.velowp input[type="number"], 1878 .wrap.velowp input[type="search"], 1879 .wrap.velowp textarea, 1880 .wrap.velowp select{ 1881 background-color: var(--vw-surface); 1882 color: var(--vw-text); 1883 } 1884 .wrap.velowp input:focus, 1885 .wrap.velowp select:focus, 1886 .wrap.velowp textarea:focus{ 1887 border-color: var(--vw-primary); 1888 box-shadow: 0 0 0 1px var(--vw-primary); 1889 } 1890 .wrap.velowp input[readonly]{ 1891 background: var(--vw-surface-muted); 1892 font-family: Consolas, Monaco, monospace; 1893 } 1894 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="mass"] .velowp-nav__btn[data-panel="mass"], 1895 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-panel="setup"] .velowp-nav__btn[data-panel="setup"], 1896 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="mass"] .velowp-nav__btn[data-panel="mass"], 1897 body.js .velowp-shell:not(.velowp-shell--js)[data-velowp-avif-panel="setup"] .velowp-nav__btn[data-panel="setup"], 1898 .velowp-nav__btn.is-active{ 1899 color: var(--vw-text); 1900 box-shadow: inset 0 2px 0 var(--vw-accent); 1901 } 1902 .velowp-home-accordion > summary, 1903 .velowp-help-faq summary{ 1904 border-radius: var(--vw-panel-radius); 1905 background: var(--vw-accent-bg); 1906 } 1907 .velowp-home-accordion[open] > summary, 1908 .velowp-help-faq details[open] summary{ 1909 border-radius: var(--vw-panel-radius) var(--vw-panel-radius) 0 0; 1910 background: var(--vw-accent-bg-open); 1911 } 1912 .velowp-home-accordion > summary::after, 1913 .velowp-help-faq summary::after{ 1914 color: var(--vw-accent); 1915 } 1916 .velowp-home-accordion > summary:hover, 1917 .velowp-help-faq summary:hover{ 1918 background: #ede4da; 1919 } 1920 .velowp-home-status-card[data-velowp-delivery-card]{ 1921 background: var(--vw-surface-muted); 1922 } 1923 .velowp-home-status-card[data-velowp-delivery-card].is-warning{ 1924 background: var(--vw-warning-bg); 1925 } 1926 .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head{ 1927 margin-bottom: 0; 1928 } 1929 .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head strong{ 1930 min-height: 44px; 1931 } 1932 .velowp-home-status-card--strategy[data-velowp-delivery-card] .velowp-home-status-card__head{ 1933 margin-bottom: 10px; 1934 } 1935 .wrap.velowp select.velowp-home-badge-select{ 1936 background-color: var(--vw-surface); 1937 border-color: var(--vw-control-border); 1938 } 1939 .wrap.velowp select.velowp-home-badge-select.is-success{ 1940 background-color: var(--vw-success-bg); 1941 border-color: var(--vw-success); 1942 color: var(--vw-success-text); 1943 } 1944 .wrap.velowp .velowp-savings-value.is-success, 1945 .velowp-quick-result-row--savings .velowp-quick-result-value{ 1946 color: var(--vw-success-text); 1947 font-weight: 600; 1948 } 1949 .wrap.velowp .velowp-savings-value.is-warning{ 1950 color: var(--vw-warning-text); 1951 } 1952 .velowp-home-generation-result [data-velowp-home-phase]:not([hidden]){ 1953 display: inline-flex; 1954 align-items: center; 1955 padding: 3px 8px; 1956 border-radius: var(--vw-radius); 1957 background: var(--vw-surface-muted); 1958 color: var(--vw-muted); 1959 } 1960 .velowp-home-generation-result [data-velowp-home-phase="running"]:not([hidden]), 1961 .velowp-home-generation-result [data-velowp-home-phase="preparing"]:not([hidden]), 1962 .velowp-home-generation-result [data-velowp-home-phase="stopping"]:not([hidden]){ 1963 background: var(--vw-info-bg); 1964 color: var(--vw-primary-dark); 1965 } 1966 .velowp-home-generation-result [data-velowp-home-phase="completed"]:not([hidden]){ 1967 background: var(--vw-success-bg); 1968 color: var(--vw-success-text); 1969 } 1970 .velowp-home-generation-result [data-velowp-home-phase="failed"]:not([hidden]){ 1971 background: var(--vw-danger-bg); 1972 color: var(--vw-danger-text); 1973 } 1974 .velowp-home-generation-result [data-velowp-home-notice], 1975 .velowp-statistics-frozen > [role="status"], 1976 .velowp-self-test-result > p.description:only-child, 1977 .velowp-url-audit-result > p.description:only-child{ 1978 padding: 10px 12px; 1979 border-radius: var(--vw-radius); 1980 background: var(--vw-info-bg); 1981 color: var(--vw-primary-dark); 1982 } 1983 .velowp-self-test-result > p.description:only-child::before, 1984 .velowp-url-audit-result > p.description:only-child::before{ 1985 content: "\f348"; 1986 display: inline-block; 1987 margin-right: 8px; 1988 font: normal 16px/1 dashicons; 1989 vertical-align: -2px; 1990 color: var(--vw-primary); 1991 } 1992 .velowp-home-status-card[data-velowp-delivery-card].is-warning #velowp-delivery-description, 1993 .velowp-home-status-card[data-velowp-delivery-card].is-warning > p.description{ 1994 color: var(--vw-warning-text); 1995 } 1996 .wrap.velowp .velowp-home-accordion__body > .form-table{ 1997 margin-top: 16px; 1998 border-spacing: 0; 1999 border-radius: var(--vw-panel-radius); 2000 background: var(--vw-surface-muted); 2001 } 2002 .wrap.velowp .velowp-home-accordion__body > .form-table th, 2003 .wrap.velowp .velowp-home-accordion__body > .form-table td{ 2004 padding: 16px; 2005 } 2006 .velowp-url-audit-form + p.description + p.description{ 2007 padding: 10px 12px; 2008 border-radius: var(--vw-radius); 2009 background: var(--vw-warning-bg); 2010 color: var(--vw-warning-text); 2011 } 2012 .wrap.velowp .notice{ 2013 border: 1px solid var(--vw-border-soft); 2014 border-radius: var(--vw-radius); 2015 background: var(--vw-surface-muted); 2016 box-shadow: none; 2017 } 2018 .wrap.velowp .notice-success{ 2019 background: var(--vw-success-bg); 2020 color: var(--vw-success-text); 2021 } 2022 .wrap.velowp .notice-warning{ 2023 background: var(--vw-warning-bg); 2024 color: var(--vw-warning-text); 2025 } 2026 .wrap.velowp .notice-error{ 2027 background: var(--vw-danger-bg); 2028 color: var(--vw-danger-text); 2029 } 2030 .wrap.velowp .notice-info{ 2031 background: var(--vw-info-bg); 2032 color: var(--vw-primary-dark); 2033 } 2034 .wrap.velowp .widefat{ 2035 background: var(--vw-surface); 2036 } 2037 .wrap.velowp .widefat thead th{ 2038 background: var(--vw-accent-bg); 2039 } 2040 .wrap.velowp .widefat.striped > tbody > :nth-child(odd){ 2041 background: var(--vw-surface-muted); 2042 } 2043 .velowp-folders-table tbody tr:not(.velowp-folder-details):hover td{ 2044 background: var(--vw-accent-bg); 2045 } 2046 .velowp-bars__value, 2047 .velowp-summarybox__v, 2048 .velowp-progressline__pct, 2049 .velowp-home-generation-result strong{ 2050 font-variant-numeric: tabular-nums; 2051 } 2052 @media (max-width: 1100px){ 2053 .velowp-home-status-card[data-velowp-delivery-card] .velowp-home-status-card__head strong{ 2054 min-height: 0; 2055 } 2056 } 2057 @media (max-width: 782px){ 2058 .wrap.velowp .velowp-home-accordion__body > .form-table th, 2059 .wrap.velowp .velowp-home-accordion__body > .form-table td{ 2060 padding: 12px; 2061 } 2062 .wrap.velowp .velowp-home-accordion__body > .form-table th{ 2063 padding-bottom: 0; 2064 } 2065 .wrap.velowp .velowp-home-accordion__body > .form-table td{ 2066 padding-top: 8px; 2067 } 2068 } -
veloimg/trunk/assets/js/admin-avif.js
r3728637 r3739629 1 1 (function () { 2 2 'use strict'; 3 var nativeSetTimeout = window.setTimeout || globalThis.setTimeout; 4 var nativeClearTimeout = window.clearTimeout || globalThis.clearTimeout; 5 function mount(pageScope) { 6 var setTimeout = pageScope ? pageScope.setTimeout : nativeSetTimeout; 7 var clearTimeout = pageScope ? pageScope.clearTimeout : nativeClearTimeout; 8 function listen(target, name, fn, options) { 9 if (pageScope) pageScope.listen(target, name, fn, options); 10 else target.addEventListener(name, fn, options); 11 } 12 if (pageScope) pageScope.onDispose(function () { 13 qsa('.velowp-popover').forEach(function (node) { node.remove(); }); 14 }); 3 15 4 16 var running = false; … … 46 58 function clearReconciliation() { 47 59 if (reconciliationTimer) { 48 window.clearTimeout(reconciliationTimer);60 clearTimeout(reconciliationTimer); 49 61 reconciliationTimer = null; 50 62 } … … 55 67 function clearRuntimeClock() { 56 68 if (runtimeClockTimer) { 57 window.clearTimeout(runtimeClockTimer);69 clearTimeout(runtimeClockTimer); 58 70 runtimeClockTimer = null; 59 71 } … … 69 81 return; 70 82 } 71 runtimeClockTimer = window.setTimeout(function () {83 runtimeClockTimer = setTimeout(function () { 72 84 runtimeClockTimer = null; 73 85 if (!running || String((currentState || {}).current_rel || '') === '') { … … 88 100 89 101 function statisticsFrozen() { 90 return !! running || !!stopRequested || !!currentState.active;102 return !!(pageScope && !pageScope.active) || !!running || !!stopRequested || !!currentState.active; 91 103 } 92 104 … … 106 118 clearFoldersOverviewRefresh(); 107 119 if (folderDetailsTimer) { 108 window.clearTimeout(folderDetailsTimer);120 clearTimeout(folderDetailsTimer); 109 121 folderDetailsTimer = null; 110 122 } … … 151 163 var row = document.querySelector('#velowp-avif-folders-overview tr[data-folder="' + openFolder.folder + '"]'); 152 164 if (!row || !row.classList.contains('velowp-folder-row--active')) { return; } 153 folderDetailsTimer = window.setTimeout(function () {165 folderDetailsTimer = setTimeout(function () { 154 166 folderDetailsTimer = null; 155 167 var currentRow = openFolder ? document.querySelector('#velowp-avif-folders-overview tr[data-folder="' + openFolder.folder + '"]') : null; … … 187 199 function clearFoldersOverviewRefresh() { 188 200 if (foldersOverviewTimer) { 189 window.clearTimeout(foldersOverviewTimer);201 clearTimeout(foldersOverviewTimer); 190 202 foldersOverviewTimer = null; 191 203 } … … 202 214 delayMs = parseInt(delayMs || 0, 10); 203 215 delayMs = Math.max(5000, delayMs || foldersOverviewRefreshMs); 204 foldersOverviewTimer = window.setTimeout(function () {216 foldersOverviewTimer = setTimeout(function () { 205 217 foldersOverviewTimer = null; 206 218 if (!folderStatsRuntime.shouldRequestOverview(typeof document !== 'undefined' && document.hidden)) { … … 333 345 334 346 function refreshNonce(done) { 347 if (pageScope && !pageScope.active) return; 335 348 if (typeof done === 'function') { 336 349 nonceWaiters.push(done); … … 378 391 flushNonceWaiters(false, explainAjaxFailure(xhr, 'timeout')); 379 392 }; 393 if (pageScope) pageScope.trackXhr(xhr); 380 394 xhr.send('action=' + encodeURIComponent(settings.nonceAction || 'velowp_avif_mass_nonce')); 381 395 } … … 395 409 396 410 function post(action, data, done, meta) { 411 if (pageScope && !pageScope.active) return; 397 412 if (avifTestHooks && typeof avifTestHooks.post === 'function') { 398 413 avifTestHooks.post(action, Object.assign({}, data || {}), done, Object.assign({}, meta || {})); … … 439 454 nextMeta._retriedNetwork = true; 440 455 finished = true; 441 window.setTimeout(function () {456 setTimeout(function () { 442 457 post(action, data, done, nextMeta); 443 458 }, 1600); … … 491 506 failOnce('timeout'); 492 507 }; 508 if (pageScope) pageScope.trackXhr(xhr, action); 493 509 xhr.send(encoded.join('&')); 494 510 } … … 1037 1053 showMessage((reason ? reason + ' ' : '') + avifText('startWaiting', 'Checking whether generation started. If it did not, the selected scope will start automatically when the server is free.'), 'info'); 1038 1054 renderStatisticsFreeze(); 1039 reconciliationTimer = window.setTimeout(function () {1055 reconciliationTimer = setTimeout(function () { 1040 1056 reconciliationTimer = null; 1041 1057 if (token !== runToken || stopRequested || !running) { return; } … … 1069 1085 function scheduleReconciliation(token, controlId, delay) { 1070 1086 if (reconciliationTimer) { 1071 window.clearTimeout(reconciliationTimer);1072 } 1073 reconciliationTimer = window.setTimeout(function () {1087 clearTimeout(reconciliationTimer); 1088 } 1089 reconciliationTimer = setTimeout(function () { 1074 1090 reconciliationTimer = null; 1075 1091 if (!running || stopRequested || token !== runToken || reconciliationInFlight) { … … 1155 1171 if (data.active) { 1156 1172 var delay = data.note ? 2500 : 250; 1157 window.setTimeout(function () {1173 setTimeout(function () { 1158 1174 driveRun(token); 1159 1175 }, delay); … … 1175 1191 stopRequested = true; 1176 1192 clearReconciliation(); 1177 if (folderDetailsTimer) { window.clearTimeout(folderDetailsTimer); folderDetailsTimer = null; }1193 if (folderDetailsTimer) { clearTimeout(folderDetailsTimer); folderDetailsTimer = null; } 1178 1194 clearFoldersOverviewRefresh(); 1179 1195 var token = ++runToken; … … 1379 1395 var total = parseInt(summary.total || 0, 10); 1380 1396 node.classList.toggle('velowp-hidden', total === 0); 1381 node.textContent = total ? avifText('outsideMonthsSummary', 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; AVIF: %3$d; unsuccessful: %4$d; left: %5$d.')1397 node.textContent = total ? avifText('outsideMonthsSummary', 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; AVIF: %3$d; unsuccessful: %4$d; left: %5$d.') 1382 1398 .replace('%1$d', total).replace('%2$d', parseInt(summary.processed || 0, 10)).replace('%3$d', parseInt(summary.ready || 0, 10)).replace('%4$d', parseInt(summary.failed || 0, 10)).replace('%5$d', parseInt(summary.remaining || 0, 10)) : ''; 1383 1399 } … … 1689 1705 1690 1706 function closeAvifFolderDetails() { 1691 if (folderDetailsTimer) { window.clearTimeout(folderDetailsTimer); folderDetailsTimer = null; }1707 if (folderDetailsTimer) { clearTimeout(folderDetailsTimer); folderDetailsTimer = null; } 1692 1708 var table = byId('velowp-avif-folders-overview'); 1693 1709 if (!table) { … … 1996 2012 } 1997 2013 shell.classList.add('velowp-shell--js'); 1998 function setPanel(panel) { 2014 var defaultPanel = shell.getAttribute('data-velowp-avif-panel') || 'mass'; 2015 function setPanel(panel, pushHash) { 1999 2016 panel = panel || shell.getAttribute('data-velowp-avif-panel') || 'mass'; 2000 2017 shell.setAttribute('data-velowp-avif-panel', panel); … … 2008 2025 el.classList.toggle('is-active', String(el.getAttribute('data-panel')) === panel); 2009 2026 }); 2027 if (pushHash) window.location.hash = panel; 2010 2028 } 2011 2029 shell.addEventListener('click', function (event) { … … 2015 2033 } 2016 2034 event.preventDefault(); 2017 setPanel(String(btn.getAttribute('data-panel') || 'mass')); 2018 }); 2019 setPanel(shell.getAttribute('data-velowp-avif-panel') || 'mass'); 2035 setPanel(String(btn.getAttribute('data-panel') || 'mass'), true); 2036 }); 2037 listen(window, 'hashchange', function () { 2038 var panel = String(window.location.hash || '').slice(1); 2039 if (!panel || panel === 'mass' || panel === 'setup') setPanel(panel || defaultPanel, false); 2040 }); 2041 var hash = String(window.location.hash || '').slice(1); 2042 setPanel(hash === 'mass' || hash === 'setup' ? hash : shell.getAttribute('data-velowp-avif-panel') || 'mass', false); 2020 2043 } 2021 2044 … … 2030 2053 }); 2031 2054 }); 2032 var select = byId('velowp-avif-candidate-select');2033 var input = byId('velowp-avif-quick-input');2034 var submit = byId('velowp-avif-quick-generate');2035 if (select && input) {2036 select.addEventListener('change', function () {2037 input.value = select.value || '';2038 if (submit && submit.getAttribute('data-velowp-avif-ready') === '1') {2039 submit.disabled = !input.value;2040 }2041 });2042 }2043 2055 } 2044 2056 … … 2117 2129 2118 2130 function init() { 2131 if (folderStatsRuntime) folderStatsRuntime.initGenerationConcurrency(cfg(), pageScope); 2119 2132 var more = byId('velowp-avif-folders-more'); 2120 2133 setFoldersMoreVisible(false); … … 2137 2150 initMass(); 2138 2151 initPopover(); 2139 if (!document.documentElement.dataset.velowpAvifFoldersVisibilityBound) { 2140 document.documentElement.dataset.velowpAvifFoldersVisibilityBound = '1'; 2141 document.addEventListener('visibilitychange', function () { 2152 listen(document, 'visibilitychange', function () { 2142 2153 if (!document.hidden) { requestFoldersOverview(false); } 2143 2154 }); 2144 }2145 2155 ensureFoldersOverviewRefresh(foldersOverviewRefreshMs); 2146 2156 } … … 2193 2203 activePopover = popover; 2194 2204 } 2195 document.addEventListener('click', function (event) {2205 listen(document, 'click', function (event) { 2196 2206 var button = event.target && event.target.closest ? event.target.closest('.velowp-file-q') : null; 2197 2207 if (button) { … … 2211 2221 closePopover(); 2212 2222 }); 2213 document.addEventListener('keydown', function (event) {2223 listen(document, 'keydown', function (event) { 2214 2224 if (event.key === 'Escape') { 2215 2225 closePopover(); 2216 2226 } 2217 2227 }); 2218 window.addEventListener('scroll', closePopover, true);2219 window.addEventListener('resize', closePopover);2228 listen(window, 'scroll', closePopover, true); 2229 listen(window, 'resize', closePopover); 2220 2230 } 2221 2231 … … 2236 2246 setRuntimeState: function (next) { next = next || {}; running = !!next.running; stopRequested = !!next.stopRequested; runToken = Number(next.runToken || 0); currentSummary = next.currentSummary || null; currentState = next.currentState || {}; allReady = Number(next.allReady || 0); }, 2237 2247 getRuntimeState: function () { return { running: running, stopRequested: stopRequested, runToken: runToken, currentSummary: currentSummary, currentState: currentState, allReady: allReady }; }, 2238 resetRuntimeState: function () { window.clearTimeout(foldersOverviewTimer); window.clearTimeout(folderDetailsTimer); clearReconciliation(); foldersOverviewTimer = null; folderDetailsTimer = null; running = false; stopRequested = false; runToken = 0; lifecycleRequestSequence = 0; lifecycleAppliedRevisions = {}; currentSummary = null; currentState = {}; allReady = 0; avifTestHooks.post = null; }2248 resetRuntimeState: function () { clearTimeout(foldersOverviewTimer); clearTimeout(folderDetailsTimer); clearReconciliation(); foldersOverviewTimer = null; folderDetailsTimer = null; running = false; stopRequested = false; runToken = 0; lifecycleRequestSequence = 0; lifecycleAppliedRevisions = {}; currentSummary = null; currentState = {}; allReady = 0; avifTestHooks.post = null; } 2239 2249 }; 2240 2250 if (window.__VELOIMG_AVIF_SKIP_AUTO_INIT__ === true) { return; } 2241 2251 } 2242 2252 if (document.readyState === 'loading') { 2243 document.addEventListener('DOMContentLoaded', init);2253 listen(document, 'DOMContentLoaded', init); 2244 2254 } else { 2245 2255 init(); 2246 2256 } 2257 } 2258 if (window.VeloimgAdminNavigation) window.VeloimgAdminNavigation.register(['velowp-avif'], mount); 2259 else mount(null); 2247 2260 })(); -
veloimg/trunk/assets/js/admin.js
r3728637 r3739629 1 1 (function ($) { 2 2 'use strict'; 3 var nativeSetTimeout = window.setTimeout || globalThis.setTimeout; 4 var nativeClearTimeout = window.clearTimeout || globalThis.clearTimeout; 5 function mount(pageScope) { 6 var setTimeout = pageScope ? pageScope.setTimeout : nativeSetTimeout; 7 var clearTimeout = pageScope ? pageScope.clearTimeout : nativeClearTimeout; 8 function pageAjax(options) { return pageScope ? pageScope.ajax(options) : $.ajax(options); } 9 function listen(target, name, fn, options) { 10 if (pageScope) pageScope.listen(target, name, fn, options); 11 else target.addEventListener(name, fn, options); 12 } 13 if (pageScope) pageScope.onDispose(function () { 14 $(document).off('.velowpPage'); $(window).off('.velowpPage'); 15 $('.velowp-popover').remove(); 16 }); 3 17 var folderStatsRuntime = window.VeloimgFolderStatisticsRuntime; 4 18 … … 49 63 } 50 64 51 window.setTimeout(function () {65 setTimeout(function () { 52 66 var target = document.getElementById('velowp-diagnostics'); 53 67 if (!target) { … … 87 101 } 88 102 103 var defaultPanel = String($shell.attr('data-velowp-panel') || 'mass'); 89 104 var initial = ''; 90 105 if (window.location.hash) { … … 96 111 setPanel(initial, false); 97 112 98 $(document).on('click ', '.velowp-nav__btn', function (e) {113 $(document).on('click.velowpPage', '.velowp-nav__btn', function (e) { 99 114 e.preventDefault(); 100 115 setPanel($(this).data('panel'), true); 101 116 }); 102 117 103 $(document).on('click ', '[data-velowp-switch]', function (e) {118 $(document).on('click.velowpPage', '[data-velowp-switch]', function (e) { 104 119 e.preventDefault(); 105 120 setPanel($(this).data('velowp-switch'), true); 106 121 }); 107 122 108 $(window).on('hashchange ', function () {123 $(window).on('hashchange.velowpPage', function () { 109 124 var h = String(window.location.hash || '').replace('#', ''); 110 if (h) { 111 setPanel(h, false); 112 } 125 setPanel(h || defaultPanel, false); 113 126 }); 114 127 } 115 128 116 129 $(function () { 130 if (pageScope && !pageScope.active) return; 117 131 initPanels(); 132 if (folderStatsRuntime) folderStatsRuntime.initGenerationConcurrency(window.velowpAdmin || {}, pageScope); 133 134 (function initHomeAccordions(){ 135 if (!$('.velowp-home-accordion').length) return; 136 function revealHashTarget(){ 137 var target = document.getElementById(String(window.location.hash || '').slice(1)); 138 if (!target) return; 139 var $block = $(target).closest('.velowp-home-accordion'); 140 if (!$block.length) return; 141 $block.prop('open', true); 142 target.scrollIntoView({ block: 'start' }); 143 } 144 $(window).on('hashchange.velowpHomeAccordion.velowpPage', revealHashTarget); 145 revealHashTarget(); 146 })(); 118 147 119 148 (function initHomeGenerationStatus(){ … … 125 154 var stopped = false; 126 155 var visibilityEpoch = 0; 156 var acknowledged = {}; 127 157 128 158 function schedule(delay){ … … 130 160 timer = null; 131 161 if (!stopped && !document.hidden) timer = setTimeout(refresh, delay); 162 } 163 function refreshSummaries(formats, epoch, refreshed){ 164 if (stopped || document.hidden || epoch !== visibilityEpoch) { schedule(6000); return; } 165 var format = formats.shift(); 166 if (!format) { schedule(refreshed ? 0 : 6000); return; } 167 pending = true; 168 var accepted = false; 169 pageAjax({ 170 url: config.ajaxUrl, type: 'POST', dataType: 'json', timeout: 45000, 171 data: { action: config.optimizationSummaryAction, format: format.name, nonce: config.homeSummaryNonces[format.name] } 172 }).done(function(response){ 173 if (stopped || document.hidden || epoch !== visibilityEpoch || !response || !response.success || !response.data) return; 174 var summary = response.data.global_summary; 175 if (response.data.ok && summary && summary.home_run_key === format.key) { 176 acknowledged[format.name] = format.key; 177 accepted = true; 178 } 179 }).fail(function(xhr){ 180 if (xhr && (xhr.status === 400 || xhr.status === 403)) stopped = true; 181 }).always(function(){ 182 pending = false; 183 // Reconcile a raced/failed refresh through status before trying again. 184 if (accepted) refreshSummaries(formats, epoch, true); 185 else schedule(6000); 186 }); 132 187 } 133 188 function refresh(){ … … 136 191 pending = true; 137 192 var epoch = visibilityEpoch; 138 $.ajax({ 193 var formats = []; 194 pageAjax({ 139 195 url: config.ajaxUrl, type: 'POST', dataType: 'json', timeout: 15000, 140 196 data: { action: 'velowp_home_status', nonce: config.homeStatusNonce } … … 160 216 } 161 217 }); 218 var phases = response.data.generation_phases || {}; 219 var keys = response.data.pending_refresh || {}; 220 if (config.optimizationSummaryAction && config.homeSummaryNonces) { 221 ['webp', 'avif'].forEach(function(format){ 222 var key = keys[format]; 223 if ((phases[format] === 'paused' || phases[format] === 'completed') && typeof key === 'string' && key && acknowledged[format] !== key && config.homeSummaryNonces[format]) { 224 formats.push({ name: format, key: key }); 225 } 226 }); 227 } 162 228 }).fail(function(xhr){ 163 229 if (xhr && (xhr.status === 400 || xhr.status === 403)) stopped = true; 164 230 }).always(function(){ 165 231 pending = false; 166 schedule(6000);232 refreshSummaries(formats, epoch, false); 167 233 }); 168 234 } 169 $(document).on('visibilitychange ', function(){235 $(document).on('visibilitychange.velowpPage', function(){ 170 236 visibilityEpoch++; 171 237 schedule(0); 172 238 }); 173 $(window).on('pagehide ', function(){ stopped = true; visibilityEpoch++; schedule(0); });239 $(window).on('pagehide.velowpPage', function(){ stopped = true; visibilityEpoch++; schedule(0); }); 174 240 schedule(6000); 175 241 })(); … … 183 249 }, 5000); 184 250 185 $(document).on('input change', 'input[type="range"][data-velowp-range-output]', function () {251 $(document).on('input.velowpPage change.velowpPage', 'input[type="range"][data-velowp-range-output]', function () { 186 252 var outputId = String($(this).data('velowp-range-output') || ''); 187 253 var val = String($(this).val() || ''); … … 191 257 }); 192 258 193 $(document).on('input change', 'input[type="range"][data-velowp-range]', function () {259 $(document).on('input.velowpPage change.velowpPage', 'input[type="range"][data-velowp-range]', function () { 194 260 var key = String($(this).data('velowp-range') || ''); 195 261 if (!key) return; … … 211 277 $note.prop('hidden', String($note.data('velowp-delivery-note') || '') !== method); 212 278 }); 279 var status = method === 'off' ? 'is-warning' : 'is-success'; 280 $('[data-velowp-delivery-card], #velowp-delivery-method, #velowp-image-strategy') 281 .removeClass('is-success is-warning is-error').addClass(status); 213 282 } 214 283 … … 217 286 })(); 218 287 219 $(document).on('click', '.velowp-details > summary', function (e) { 288 (function initHomeStrategyNote(){ 289 var $select = $('#velowp-image-strategy'); 290 var $note = $('#velowp-strategy-description'); 291 if (!$select.length || !$note.length) return; 292 function syncStrategyNote(){ 293 $note.text(String($select.find('option:selected').attr('data-description') || '')); 294 } 295 $select.on('change', syncStrategyNote); 296 syncStrategyNote(); 297 })(); 298 299 $(document).on('click.velowpPage', '.velowp-details > summary', function (e) { 220 300 e.preventDefault(); 221 301 var $d = $(this).closest('details'); … … 223 303 }); 224 304 225 var $input = $('#velowp-quick-input'); 305 ['', 'avif-'].forEach(function (prefix) { 306 var $input = $('#velowp-' + prefix + 'quick-input'); 226 307 if ($input.length) { 227 var $quickSubmit = $('#velowp- quick-generate');228 var $fallbackInput = $('#velowp- quick-input-fallback');308 var $quickSubmit = $('#velowp-' + prefix + 'quick-generate'); 309 var $fallbackInput = $('#velowp-' + prefix + 'quick-input-fallback'); 229 310 230 311 function syncQuickSubmitState() { … … 233 314 var ready = hiddenVal.length > 0 || fallbackVal.length > 0; 234 315 if ($quickSubmit.length) { 235 $quickSubmit.prop('disabled', !ready );316 $quickSubmit.prop('disabled', !ready || $quickSubmit.attr('data-velowp-avif-ready') === '0'); 236 317 } 237 318 } 238 319 239 320 syncQuickSubmitState(); 240 if ($('#velowp- candidate-select').length) {241 $('#velowp- candidate-select').find('option').first().text(velowpText('selectImage', 'Select an image'));242 } 243 244 $('#velowp- candidate-select').on('change', function () {321 if ($('#velowp-' + prefix + 'candidate-select').length) { 322 $('#velowp-' + prefix + 'candidate-select').find('option').first().text(velowpText('selectImage', 'Select an image')); 323 } 324 325 $('#velowp-' + prefix + 'candidate-select').on('change', function () { 245 326 var v = String($(this).val() || ''); 246 327 $input.val(v); … … 248 329 }); 249 330 250 $('#velowp- pick-media').on('click', function (e) {331 $('#velowp-' + prefix + 'pick-media').on('click', function (e) { 251 332 e.preventDefault(); 252 333 if (typeof wp === 'undefined' || !wp.media) { … … 286 367 } 287 368 } 369 }); 370 371 (function initBypassCopy(){ 372 var $field = $('#velowp-bypass-query'); 373 var $status = $('#velowp-bypass-copy-status'); 374 if (!$field.length) return; 375 function report(copied){ 376 $status.text(String($status.attr(copied ? 'data-success' : 'data-fallback') || '')); 377 } 378 function fallback(){ 379 $field[0].focus(); 380 $field[0].select(); 381 var copied = false; 382 try { copied = !!document.execCommand && document.execCommand('copy'); } catch (e) {} 383 report(copied); 384 } 385 function copy(){ 386 $field[0].select(); 387 if (window.navigator.clipboard && typeof window.navigator.clipboard.writeText === 'function') { 388 window.navigator.clipboard.writeText(String($field.val() || '')).then(function(){ report(true); }, fallback); 389 } else { 390 fallback(); 391 } 392 } 393 $field.on('click', copy); 394 $('#velowp-bypass-copy').on('click', copy); 395 })(); 288 396 289 397 (function initSelfTest(){ … … 356 464 $result.html('<p class="description velowp-muted">' + velowpEscapeHtml(velowpText('selfTestLoading', 'Running self-test...')) + '</p>'); 357 465 358 $.ajax({466 pageAjax({ 359 467 method: 'POST', 360 468 url: ajaxUrl, … … 522 630 $result.html('<p class="description velowp-muted">' + velowpEscapeHtml(velowpText('frontendAuditLoading', 'Checking the page...')) + '</p>'); 523 631 524 $.ajax({632 pageAjax({ 525 633 method: 'POST', 526 634 url: ajaxUrl, … … 550 658 })(); 551 659 552 $(document).on('change ', '.velowp-scan-candidate', function () {660 $(document).on('change.velowpPage', '.velowp-scan-candidate', function () { 553 661 var v = String($(this).val() || ''); 554 662 if (v) { … … 608 716 var $badges = $('#velowp-mass-badges'); 609 717 var $help = $('#velowp-mass-help'); 718 var $retryCard = $('#velowp-webp-retry'); 719 var $retryStart = $('#velowp-webp-retry-start'); 720 var $retryQuality = $('#velowp-webp-retry-quality'); 721 var $retryEngine = $('#velowp-webp-retry-engine'); 722 var webpRetrySnapshot = { scope: '', count: 0 }; 610 723 var $outsideMonthsSummary = $('#velowp-outside-months-summary'); 611 724 … … 723 836 724 837 function statisticsFrozen() { 725 return ! webpControlState || !!webpCommandPending || webpControlUncertain || webpStopUnconfirmed838 return !!(pageScope && !pageScope.active) || !webpControlState || !!webpCommandPending || webpControlUncertain || webpStopUnconfirmed 726 839 || ['preparing', 'running', 'stopping'].indexOf(webpPhase(webpControlState)) !== -1; 727 840 } … … 818 931 renderStatisticsFreeze(); 819 932 applyModeUI(); 933 renderWebpRetry(); 820 934 $stop.prop('disabled', !!webpCommandPending || webpControlUncertain || !state.control_id || (phase !== 'running' && phase !== 'preparing')); 821 935 var label = '', buttonLabel = ''; … … 857 971 if (statusText.indexOf(webpControlError + ' ') !== 0) $status.text(webpControlError + ' ' + statusText); 858 972 } 973 } 974 975 function renderWebpRetry() { 976 var state = webpControlState || {}; 977 var ownRetry = !!state.retry && !!state.retry.quality && isSameRunScope(state, sourceValue(), folderValue()); 978 var activeRetry = ownRetry && ['preparing', 'running', 'stopping'].indexOf(webpPhase(state)) !== -1; 979 var count = webpRetrySnapshot.scope === currentWebpScopeKey() ? webpRetrySnapshot.count : 0; 980 if (activeRetry) count = progressInt(state.manifest_count || state.display_total || state.total); 981 $retryCard.toggleClass('velowp-hidden', count <= 0); 982 $('#velowp-webp-retry-count').text(massText('retryCount', 'Files eligible for retry in this selection: %d').replace('%d', count)); 983 var disabled = statisticsFrozen() || running || !massStateReady || webpStatsFlight === webpUiEpoch; 984 $retryCard.find('input, select').prop('disabled', disabled); 985 $retryStart.prop('disabled', disabled || count <= 0 || $retryCard.attr('data-encoder-ready') !== '1') 986 .text(massText('retryStart', 'Retry processing %d files').replace('%d', count)); 987 if (activeRetry) { 988 $retryQuality.val(state.retry.quality); 989 $retryEngine.val(state.retry.engine); 990 } 991 $('#velowp-webp-retry-quality-value').text(String($retryQuality.val() || 75) + '%'); 992 var hint = disabled ? massText(activeRetry ? 'retryRunning' : 'retryUnavailable', activeRetry 993 ? 'Retry processing is running. Progress and Stop are in the generation block above.' 994 : 'Retry processing will be available after statistics are updated and generation is stopped or completed.') : ''; 995 $('#velowp-webp-retry-status').toggleClass('velowp-hidden', !hint).text(hint); 859 996 } 860 997 … … 1181 1318 return; 1182 1319 } 1183 var text = massText('outsideMonthsSummary', 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.')1320 var text = massText('outsideMonthsSummary', 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.') 1184 1321 .replace('%1$d', total).replace('%2$d', parseInt(outside.processed || 0, 10)) 1185 1322 .replace('%3$d', parseInt(outside.ready || 0, 10)).replace('%4$d', parseInt(outside.failed || 0, 10)).replace('%5$d', parseInt(outside.remaining || 0, 10)); … … 1377 1514 return; 1378 1515 } 1379 nonceRefreshXhr = $.ajax({1516 nonceRefreshXhr = pageAjax({ 1380 1517 url: ajaxUrl, 1381 1518 type: 'POST', … … 1410 1547 data.action = action; 1411 1548 data.nonce = nonce; 1412 $.ajax({1549 pageAjax({ 1413 1550 url: ajaxUrl, 1414 1551 type: 'POST', … … 1824 1961 1825 1962 setFoldersMoreVisible(false); 1826 $(document).off('click.velowpFoldersMore ').on('click.velowpFoldersMore', '#velowp-folders-more', function () {1963 $(document).off('click.velowpFoldersMore.velowpPage').on('click.velowpFoldersMore.velowpPage', '#velowp-folders-more', function () { 1827 1964 if (statisticsFrozen() || folderMorePending) { return; } 1828 1965 folderHistoryAutoContinued = false; … … 2170 2307 } 2171 2308 2172 $(document).off('click.velowpSort ').on('click.velowpSort', '#velowp-folders-overview .velowp-folder-details th.velowp-sort', function(e){2309 $(document).off('click.velowpSort.velowpPage').on('click.velowpSort.velowpPage', '#velowp-folders-overview .velowp-folder-details th.velowp-sort', function(e){ 2173 2310 e.preventDefault(); 2174 2311 if (!openFolder) return; … … 2361 2498 return isThemeMode() 2362 2499 ? massText('emptyThemeFolder', 'No JPG/PNG images were found in the selected theme folder.') 2363 : massText('emptyFolder', 'No images were found in the Media Libraryfor this folder.');2500 : massText('emptyFolder', 'No images were found in uploads for this folder.'); 2364 2501 } 2365 2502 … … 2369 2506 } 2370 2507 return isAllMode() 2371 ? massText('startAll', 'Start for the entire Media Library')2508 ? massText('startAll', 'Start for all uploads') 2372 2509 : massText('startFolder', 'Start generation'); 2373 2510 } … … 2495 2632 } else { 2496 2633 2497 $start.text(all ? massText('startAll', 'Start for the entire Media Library') : massText('startFolder', 'Start generation'));2634 $start.text(all ? massText('startAll', 'Start for all uploads') : massText('startFolder', 'Start generation')); 2498 2635 } 2499 2636 if (theme && (controlsLocked || massStateReady)) { … … 2669 2806 2670 2807 function requestStats() { 2808 renderWebpRetry(); 2671 2809 if (webpCommandPending) return; 2672 2810 if (webpStatsFlight === webpUiEpoch) return; … … 2707 2845 updateSecondaryActions(folderVal, {}); 2708 2846 webpStatsFlight = requestedEpoch; 2847 renderWebpRetry(); 2709 2848 if (!statisticsFrozen()) $('#velowp-statistics-status').removeClass('velowp-hidden').text(massText('statsUpdating', 'Updating statistics...')); 2710 2849 ajax('velowp_mass_stats', { folder: folderVal, source: sourceValue(), include_overview: '1', history_limit: folderHistoryLimit }, function (d) { … … 2864 3003 renderOutsideMonthsSummary(summary); 2865 3004 renderHelpBlock(summary); 3005 webpRetrySnapshot = { scope: currentWebpScopeKey(), count: progressInt(summary.retryable) }; 3006 renderWebpRetry(); 2866 3007 2867 3008 … … 2918 3059 $status.text(massText('emptyThemeFolder', 'No JPG/PNG images were found in the selected theme folder.')); 2919 3060 } else { 2920 $status.text(massText('emptyFolder', 'No images were found in the Media Libraryfor this folder.'));3061 $status.text(massText('emptyFolder', 'No images were found in uploads for this folder.')); 2921 3062 } 2922 3063 } else { … … 3038 3179 payload.source = sourceValue(); 3039 3180 payload.mode = mode === 'repair' ? 'repair' : 'generate'; 3181 if (action === 'start' && mode === 'retry') { 3182 payload.retry_quality = String($retryQuality.val() || 75); 3183 payload.retry_engine = String($retryEngine.val() || 'auto'); 3184 payload.display_total = webpRetrySnapshot.count; 3185 payload.display_ready = payload.display_failed = payload.display_processed = 0; 3186 } 3040 3187 } 3041 3188 var epoch = bumpWebpUiEpoch(); … … 3127 3274 } 3128 3275 startRun('generate'); 3276 }); 3277 $retryQuality.on('input change', function () { $('#velowp-webp-retry-quality-value').text(String(this.value) + '%'); }); 3278 $retryStart.on('click', function (e) { 3279 e.preventDefault(); 3280 if ($retryStart.prop('disabled') || webpRetrySnapshot.scope !== currentWebpScopeKey() || webpRetrySnapshot.count <= 0) return; 3281 runWebpCommand('start', 'retry'); 3129 3282 }); 3130 3283 if ($repair.length) { … … 3200 3353 } 3201 3354 3202 $(window).on('focus ', function () {3355 $(window).on('focus.velowpPage', function () { 3203 3356 refreshMassStateFromServer(); 3204 3357 }); 3205 3358 if (document && document.addEventListener) { 3206 document.addEventListener('visibilitychange', function () {3359 listen(document, 'visibilitychange', function () { 3207 3360 if (!document.hidden) { 3208 3361 refreshMassStateFromServer(); … … 3221 3374 renderGlobalSummary(); 3222 3375 applyModeUI(); 3223 setTimeout(requestStats, 80);3376 setTimeout(requestStats, 0); 3224 3377 // The first status response establishes whether reads are safe and carries the overview. 3225 3378 })(); }); … … 3256 3409 activePopover = $p; 3257 3410 } 3258 $(document).on('click ', '.velowp-file-q, .velowp-help-q', function(e){3411 $(document).on('click.velowpPage', '.velowp-file-q, .velowp-help-q', function(e){ 3259 3412 e.preventDefault(); 3260 3413 e.stopPropagation(); … … 3267 3420 openPopover($btn, text); 3268 3421 }); 3269 $(document).on('click ', function(e){3422 $(document).on('click.velowpPage', function(e){ 3270 3423 if (e && $(e.target).closest('.velowp-popover, .velowp-file-q, .velowp-help-q').length) return; 3271 3424 closePopover(); 3272 3425 }); 3273 $(document).on('keydown', function(e){ if (e && e.key === 'Escape') closePopover(); }); 3274 $(window).on('scroll resize', function(){ if (activePopover) closePopover(); }); 3275 3426 $(document).on('keydown.velowpPage', function(e){ if (e && e.key === 'Escape') closePopover(); }); 3427 $(window).on('scroll.velowpPage resize.velowpPage', function(){ if (activePopover) closePopover(); }); 3428 3429 } 3430 if (window.VeloimgAdminNavigation) window.VeloimgAdminNavigation.register(['velowp', 'velowp-webp', 'velowp-help'], mount); 3431 else mount(null); 3276 3432 })(jQuery); -
veloimg/trunk/assets/js/folder-statistics-runtime.js
r3728637 r3739629 74 74 function overviewVisible(hasRows, renderedSource, renderedFolder, source, folder) { 75 75 return !!hasRows && (renderedSource || 'uploads') === source && (source !== 'theme' || renderedFolder === folder); 76 } 77 78 var dismissedGenerations = {}; 79 /** One display-only warning for both generation pages; never changes controls. */ 80 function initGenerationConcurrency(config, pageScope) { 81 if (typeof document === 'undefined') return; 82 var node = document.querySelector('[data-velowp-concurrency-warning]'); 83 if (!node || !config.ajaxUrl || !config.homeStatusNonce || node.velowpConcurrencyInitialized) return; 84 var other = node.getAttribute('data-velowp-other-format'); 85 if (other !== 'webp' && other !== 'avif') return; 86 node.velowpConcurrencyInitialized = true; 87 var generationId = node.getAttribute('data-velowp-generation-id') || ''; 88 var storageKey = 'veloimg:concurrency-dismissed:' + config.ajaxUrl + ':' + other; 89 var dismissed = dismissedGenerations[storageKey] || ''; 90 try { dismissed = window.sessionStorage.getItem(storageKey) || dismissed; } catch (error) { /* Storage may be disabled; AJAX navigation still keeps the dismissal. */ } 91 var unknownDismissed = false; 92 if (generationId && dismissed === generationId) node.hidden = true; 93 function dismiss() { 94 node.hidden = true; 95 if (!generationId) { unknownDismissed = true; return; } 96 dismissed = generationId; 97 dismissedGenerations[storageKey] = generationId; 98 try { window.sessionStorage.setItem(storageKey, generationId); } catch (error) { /* Keep the in-memory dismissal. */ } 99 } 100 var close = node.querySelector('[data-velowp-concurrency-dismiss]'); 101 if (close) { 102 if (pageScope) pageScope.listen(close, 'click', dismiss); 103 else close.addEventListener('click', dismiss); 104 } 105 var timer = null, pending = false, stopped = false, epoch = 0; 106 if (pageScope) pageScope.onDispose(function () { stopped = true; epoch++; if (timer !== null) clearTimeout(timer); }); 107 function schedule(delay) { 108 if (timer !== null) clearTimeout(timer); 109 timer = null; 110 if (!stopped && !document.hidden) timer = setTimeout(refresh, delay); 111 } 112 function refresh() { 113 timer = null; 114 if (stopped || document.hidden || pending) return; 115 pending = true; 116 var requestEpoch = epoch, xhr = new XMLHttpRequest(); 117 xhr.open('POST', config.ajaxUrl, true); 118 xhr.timeout = 15000; 119 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); 120 function finish() { 121 pending = false; 122 if (xhr.status === 400 || xhr.status === 403) stopped = true; 123 if (!stopped && !document.hidden && requestEpoch === epoch && xhr.status >= 200 && xhr.status < 300) { 124 try { 125 var response = JSON.parse(xhr.responseText || '{}'); 126 var phase = response.success && response.data && response.data.generation_phases && response.data.generation_phases[other]; 127 // Missing/failed reads retain the last warning instead of claiming idle. 128 if (['idle', 'preparing', 'running', 'stopping', 'paused', 'failed', 'completed'].indexOf(phase) !== -1) { 129 var ids = response.data.generation_ids; 130 if (ids && typeof ids[other] === 'string') { 131 if (ids[other] !== generationId) unknownDismissed = false; 132 generationId = ids[other]; 133 } 134 if (phase === 'idle' || phase === 'completed' || phase === 'failed') unknownDismissed = false; 135 var active = ['preparing', 'running', 'stopping'].indexOf(phase) !== -1; 136 node.hidden = !active || unknownDismissed || !!(generationId && dismissed === generationId); 137 } 138 } catch (error) { /* Keep the last confirmed display state. */ } 139 } 140 schedule(6000); 141 } 142 xhr.onload = xhr.onerror = xhr.ontimeout = finish; 143 xhr.send('action=velowp_home_status&nonce=' + encodeURIComponent(config.homeStatusNonce)); 144 } 145 var visible = function () { epoch++; schedule(0); }; 146 var leave = function () { stopped = true; epoch++; schedule(0); }; 147 if (pageScope) { 148 pageScope.listen(document, 'visibilitychange', visible); 149 pageScope.listen(window, 'pagehide', leave); 150 } else { 151 document.addEventListener('visibilitychange', visible); 152 window.addEventListener('pagehide', leave); 153 } 154 schedule(0); 76 155 } 77 156 … … 119 198 overviewRefreshDelay: 6000, 120 199 overviewVisible: overviewVisible, 200 initGenerationConcurrency: initGenerationConcurrency, 121 201 renderOptimizationSummary: renderOptimizationSummary, 122 202 refreshOptimizationSummary: refreshOptimizationSummary, -
veloimg/trunk/languages/veloimg-ru_RU.po
r3728637 r3739629 2 2 msgid "" 3 3 msgstr "" 4 "Project-Id-Version: VeloIMG – WebP & AVIF Image Optimizer 0.9. 6\n"4 "Project-Id-Version: VeloIMG – WebP & AVIF Image Optimizer 0.9.7\n" 5 5 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/veloimg\n" 6 6 "POT-Creation-Date: 2026-09-27T08:44:59+00:00\n" … … 3328 3328 msgid "%1$d processed · %2$d optimized · %3$d unsuccessful" 3329 3329 msgstr "%1$d обработано · %2$d оптимизировано · %3$d неудачно" 3330 3331 #: templates/admin-home.php 3332 msgid "Diagnostics" 3333 msgstr "Диагностика" 3334 3335 #: templates/admin-diagnostics.php 3336 msgid "Trial WebP" 3337 msgstr "Пробный WebP" 3338 3339 #: templates/admin-diagnostics.php 3340 msgid "Trial AVIF" 3341 msgstr "Пробный AVIF" 3342 3343 #: templates/admin-home.php 3344 msgid "View original images" 3345 msgstr "Просмотр оригиналов" 3346 3347 #: templates/admin-home.php 3348 msgid "Copy parameter" 3349 msgstr "Скопировать параметр" 3350 3351 #: templates/admin-home.php 3352 msgid "Copied." 3353 msgstr "Скопировано." 3354 3355 #: templates/admin-home.php 3356 msgid "Parameter selected. Copy it with Ctrl+C or Command+C." 3357 msgstr "Параметр выделен. Скопируйте его сочетанием Ctrl+C или Command+C." 3358 3359 #: templates/admin-home.php 3360 msgid "Copy the parameter and append it to the page address to view the original images. Saved settings are not changed." 3361 msgstr "Скопируйте параметр и добавьте его в конец адреса страницы, чтобы увидеть оригинальные изображения. Сохранённые настройки не меняются." 3362 3363 #: templates/admin-home.php 3364 msgid "Copy the parameter and append it to the JPG/PNG image address to view the original. Saved settings are not changed." 3365 msgstr "Скопируйте параметр и добавьте его в конец адреса JPG/PNG-изображения, чтобы увидеть оригинал. Сохранённые настройки не меняются." 3366 3367 #: templates/admin-home.php 3368 msgid "Controls how much diagnostic detail is written to the server error log. Leave Error for normal use; Warning and Info help investigate problems, for example at support request. If WordPress debug logging is enabled, messages appear in wp-content/debug.log." 3369 msgstr "Определяет подробность записей в журнале ошибок сервера. Для обычной работы оставьте «Ошибки». «Предупреждения» и «Информация» помогают разобраться в проблемах, например по просьбе поддержки. Если в WordPress включён журнал отладки, записи доступны в wp-content/debug.log." 3370 3371 #: templates/admin-dashboard.php 3372 msgid "Configure generation here. Single-file tests are in Diagnostics on the VeloIMG home page." 3373 msgstr "Здесь настраивается генерация. Пробная обработка одного файла доступна в блоке «Диагностика» на главной странице VeloIMG." 3374 3375 #: templates/webp-retry.php 3376 msgid "Retry unsuccessful WebP files" 3377 msgstr "Повторная обработка WebP" 3378 3379 #: templates/webp-retry.php 3380 msgid "Retry quality" 3381 msgstr "Качество повторного сжатия" 3382 3383 #: templates/webp-retry.php 3384 msgid "Encoder" 3385 msgstr "Движок" 3386 3387 #: templates/webp-retry.php 3388 msgid "Automatic" 3389 msgstr "Авто" 3390 3391 #: templates/webp-retry.php 3392 msgid "Lossy compression is used for this retry, including PNG. Original files, successful WebP files and your main settings are not changed." 3393 msgstr "Повторное сжатие с потерями, в том числе для PNG. Оригиналы, успешно созданные WebP и основные настройки не изменяются." 3394 3395 #: templates/webp-retry.php 3396 msgid "Retry processing" 3397 msgstr "Повторить обработку" 3398 3399 #: src/Admin/SettingsPage.php 3400 #, php-format 3401 msgid "Files eligible for retry in this selection: %d" 3402 msgstr "Можно обработать повторно в выбранной области: %d файлов" 3403 3404 #: src/Admin/SettingsPage.php 3405 #, php-format 3406 msgid "Retry processing %d files" 3407 msgstr "Повторить обработку %d файлов" 3408 3409 #: src/Admin/SettingsPage.php 3410 msgid "Retry processing is running. Progress and Stop are in the generation block above." 3411 msgstr "Идёт повторная обработка. Прогресс и кнопка остановки — в блоке генерации выше." 3412 3413 #: src/Admin/SettingsPage.php 3414 msgid "Retry processing will be available after statistics are updated and generation is stopped or completed." 3415 msgstr "Повторная обработка станет доступна после обновления статистики и остановки или завершения генерации." 3416 3417 #: src/Admin/SettingsPage.php 3418 msgid "No unsuccessful WebP files eligible for retry remain in this selection. Refresh the statistics." 3419 msgstr "В выбранной области больше нет неудачных WebP, подходящих для повторной обработки. Обновите статистику." 3420 3421 #: src/Admin/SettingsPage.php 3422 msgid "Choose a retry quality between 50 and 100." 3423 msgstr "Выберите качество повторного сжатия от 50 до 100." 3424 3425 #: src/Admin/SettingsPage.php 3426 msgid "The selected WebP encoder is unavailable. Refresh the page and select an available encoder." 3427 msgstr "Выбранный движок WebP недоступен. Обновите страницу и выберите доступный движок." 3428 3429 #: templates/admin-dashboard.php templates/admin-avif.php src/Admin/SettingsPage.php 3430 msgid "All uploads" 3431 msgstr "Все загрузки" 3432 3433 #: src/Admin/SettingsPage.php 3434 msgid "Start for all uploads" 3435 msgstr "Запустить для всех загрузок" 3436 3437 #: src/Admin/SettingsPage.php 3438 msgid "Loading uploads information..." 3439 msgstr "Загружаем данные изображений из uploads..." 3440 3441 #: src/Admin/SettingsPage.php 3442 msgid "No images were found in uploads for this folder." 3443 msgstr "В этой папке uploads изображения не найдены." 3444 3445 #: templates/admin-dashboard.php 3446 msgid "Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only WebP derivatives are created." 3447 msgstr "Выберите все изображения из uploads, папку месяца (YYYY/MM) или папку активной темы. Оригиналы не изменяются — создаются только WebP-деривативы." 3448 3449 #: templates/admin-avif.php 3450 msgid "Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only AVIF derivatives are created." 3451 msgstr "Выберите все изображения из uploads, папку месяца (YYYY/MM) или папку активной темы. Оригиналы не изменяются — создаются только AVIF-деривативы." 3452 3453 #: src/Admin/SettingsPage.php 3454 #, php-format 3455 msgid "Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d." 3456 msgstr "Вне папок по месяцам (входит в «Все загрузки»): %1$d изображений; обработано: %2$d; WebP: %3$d; неудачно: %4$d; осталось: %5$d." 3457 3458 #: templates/generation-concurrency.php 3459 #, php-format 3460 msgid "Generation of %s is already running. Generating WebP and AVIF at the same time may significantly slow down image processing. You can start both formats or wait until the current run finishes." 3461 msgstr "Сейчас запущена генерация %s. Одновременная генерация WebP и AVIF может существенно замедлить обработку изображений. Можно запустить оба формата или дождаться завершения." 3462 3463 #: src/Admin/SettingsPage.php 3464 msgid "Loading section..." 3465 msgstr "Загружаем раздел…" 3466 3467 #: src/Admin/SettingsPage.php 3468 msgid "Sending request..." 3469 msgstr "Отправляем запрос…" 3470 3471 #: src/Admin/SettingsPage.php 3472 msgid "Could not load the section. The current page is still available." 3473 msgstr "Не удалось загрузить раздел. Текущая страница остаётся доступной." 3474 3475 #: src/Admin/SettingsPage.php 3476 msgid "The server response could not be confirmed. Open the page to check the result before trying again." 3477 msgstr "Не удалось подтвердить ответ сервера. Откройте страницу и проверьте результат перед повторной попыткой." 3478 3479 #: src/Admin/SettingsPage.php 3480 msgid "Open page" 3481 msgstr "Открыть страницу" 3482 3483 #: src/Admin/SettingsPage.php 3484 msgid "There are unsaved settings. Leave this section without saving?" 3485 msgstr "Есть несохранённые настройки. Перейти в другой раздел без сохранения?" 3486 3487 #: templates/generation-concurrency.php 3488 msgid "Dismiss this notice." 3489 msgstr "Закрыть уведомление." -
veloimg/trunk/readme.txt
r3728637 r3739629 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.9. 67 Stable tag: 0.9.7 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 25 25 * Original JPG and PNG files remain untouched. 26 26 * Safe delivery chains with AVIF, WebP, and original-image fallback. 27 * Bulk generation for the full Media Library, monthly uploads folders, and selected theme folders.27 * Bulk generation for all uploads, monthly uploads folders, and selected theme folders. 28 28 * Pause and continue long generation runs. 29 29 * Separate WebP and AVIF processing with clear statistics and failed-file reasons. … … 46 46 * Folder-level statistics, file lists, ZIP download for selected uploads folders, and safe deletion of generated derivatives. 47 47 * Per-format optimization summaries for uploads and active/parent theme images; files without a usable derivative retain their original weight in the totals. 48 * Compact main dashboard with frontend delivery and format priority controls, collapsible setup checks, diagnostics, and a page delivery check. 49 * Navigation and settings/test submissions without a full-page reload, with native WordPress fallbacks. 50 * Contextual retry of eligible unsuccessful WebP files with run-local quality and an available encoder; original images, successful files, and saved main settings are preserved. 48 51 49 52 VeloIMG is designed for sites with many images. It avoids modifying originals and does not serve generated files when the derivative is invalid, outdated, or larger than the original where that rule applies. … … 107 110 = Can WebP and AVIF generation run at the same time? = 108 111 109 Yes. WebP and AVIF use separate generation flows. On limited shared hosting, running them one after another is recommended to reduce server load. 112 Yes. WebP and AVIF use separate generation flows, but running both at once can slow image processing. The generation screen explains this before starting the second format; the notice can be dismissed for the current generation. 113 114 = Can I retry unsuccessful WebP files with different settings? = 115 116 Yes. When eligible unsuccessful files exist in the selected uploads or theme scope, a retry block appears below generation. Choose retry quality and, when both are available, Imagick or GD. This retry uses lossy compression, including PNG. Original images, successful WebP files, and the main generation settings are not changed. Missing or unreadable source images cannot be repaired by changing quality. 110 117 111 118 = Does VeloIMG delete generated files? = … … 116 123 117 124 1. Main VeloIMG dashboard with server readiness, frontend delivery, format priority, and setup checks. 118 2. WebP section with generation settings, folder selection, progress, statistics, and file management.119 3. AVIF section with generation settings, folder selection, progress, statistics, and file management.125 2. WebP generation with uploads/theme selection, progress, optimization summary, and file management. 126 3. AVIF generation with uploads/theme selection, progress, optimization summary, and file management. 120 127 4. Frontend delivery check showing page response, format coverage, derivative URLs, and unavailable files. 121 5. Folder statistics with processed, successful, skipped, and unsuccessful images.128 5. Folder statistics with an expanded file list, status, original and WebP sizes, and savings. 122 129 123 130 == Changelog == 131 132 = 0.9.7 = 133 134 * Updated the admin interface with a warm, readable visual system and consistent WebP/AVIF controls, status badges, and notices. 135 * Moved frontend delivery and format priority controls into the main dashboard, with Save settings directly below them and AVIF-first defaults for new or missing choices; explicit saved choices are preserved. 136 * Added collapsible setup, diagnostics, and frontend URL-check blocks on the main page. WebP/AVIF trial reports remain independently available after navigation, with a copyable delivery-bypass parameter and an explained log setting. 137 * Added contextual retry of eligible unsuccessful WebP files with separate quality and available Imagick/GD selection, without changing originals, successful files, or main settings. 138 * Added navigation and settings/test submissions without a full-page reload, retaining native WordPress forms, URL history, and safe recovery when requests fail. 139 * Improved completion-state refresh on the main dashboard and dimmed saved optimization figures while generation or statistics refresh is in progress. 140 * Renamed the whole-upload scope to All uploads and added a dismissible warning that concurrent WebP/AVIF generation can slow image processing. 141 * Prevented setup/generation panels flashing together on first render, stabilized navigation loading notices, and corrected the reported localization, nonce, input sanitation, and output-escaping warnings. 142 * Updated the bundled Russian translation and the English/Russian screenshots. Existing generation settings and original files remain unchanged. 124 143 125 144 = 0.9.6 = … … 188 207 == Upgrade Notice == 189 208 209 = 0.9.7 = 210 211 Updates the dashboard, diagnostics, WebP retry controls, and navigation. Stop both bulk generators before updating. Existing settings and original images are preserved; clear cached admin assets if the old interface remains visible. 212 190 213 = 0.9.6 = 191 214 -
veloimg/trunk/src/Admin/OptimizationSummary.php
r3728637 r3739629 10 10 if ( ! wp_verify_nonce( $nonce, 'velowp_home_status' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', 'veloimg' ) ), 400 ); } 11 11 $cards = array(); 12 $phases = array(); 13 $generation_ids = array(); 14 $pending_refresh = array(); 12 15 foreach ( array( 'webp', 'avif' ) as $format ) { 13 16 $context = self::home_context( $format ); 14 17 // A read failure is not an empty run: the browser retains its previous card. 15 if ( $context['available'] ) { $cards[ $format ] = self::home_html( $format, $context ); } 18 if ( $context['available'] ) { 19 $cards[ $format ] = self::home_html( $format, $context ); 20 $phases[ $format ] = $context['phase']; 21 $generation_ids[ $format ] = $context['generation_id']; 22 if ( $context['statistics_pending'] ) { $pending_refresh[ $format ] = $context['run_key']; } 23 } 16 24 } 17 wp_send_json_success( array( 'cards' => $cards ) );25 wp_send_json_success( array( 'cards' => $cards, 'generation_phases' => $phases, 'generation_ids' => $generation_ids, 'pending_refresh' => $pending_refresh ) ); 18 26 } 19 27 … … 28 36 /** The same saved format summary as generation pages; only status is live. */ 29 37 public static function home_context( string $format ) : array { 30 $context = array( 'global' => self::read( $format ), 'phase' => 'idle', 'available' => true, 'statistics_pending' => false );38 $context = array( 'global' => self::read( $format ), 'phase' => 'idle', 'available' => true, 'statistics_pending' => false, 'run_key' => '', 'generation_id' => '' ); 31 39 try { 32 40 $run = self::home_run( $format ); 33 41 $context['phase'] = $run['phase']; 42 $context['run_key'] = $run['key']; 43 $context['generation_id'] = $run['id']; 34 44 // A terminal status is not proof that its ordinary statistics refresh finished. 35 45 $context['statistics_pending'] = in_array( $run['phase'], array( 'paused', 'completed' ), true ) … … 48 58 } else { 49 59 $state = self::fresh_home_option( 'velowp_avif_mass_state_v2' ); 50 if ( null === $state || array() === $state ) { return array( 'phase' => 'idle', 'key' => self::home_run_key( $format, array() ) ); }60 if ( null === $state || array() === $state ) { return array( 'phase' => 'idle', 'key' => self::home_run_key( $format, array() ), 'id' => '' ); } 51 61 if ( ! is_array( $state ) ) { throw new \UnexpectedValueException( 'home_state_invalid' ); } 52 62 $phase = ! empty( $state['runtime_failure'] ) ? 'failed' : ( ! empty( $state['stopped'] ) ? 'paused' : ( ! empty( $state['done'] ) ? 'completed' : ( ! empty( $state['active'] ) ? 'running' : 'paused' ) ) ); … … 55 65 } 56 66 if ( ! in_array( $phase, array( 'idle', 'preparing', 'running', 'stopping', 'paused', 'failed', 'completed' ), true ) ) { throw new \UnexpectedValueException( 'home_phase_invalid' ); } 57 return array( 'phase' => $phase, 'key' => self::home_run_key( $format, $state ) ); 67 return array( 'phase' => $phase, 'key' => self::home_run_key( $format, $state ), 'id' => self::generation_id( $format, $state ) ); 68 } 69 70 /** Display-only dismissal identity: progress and Stop/Continue never change it. */ 71 private static function generation_id( string $format, array $state ) : string { 72 $id = (string) ( $state[ 'webp' === $format ? 'run_id' : 'manifest_id' ] ?? '' ); 73 if ( '' === $id && ! empty( $state['started_at'] ) ) { 74 $id = implode( '|', array( (string) ( $state['source'] ?? '' ), (string) ( $state['folder'] ?? '' ), (string) $state['started_at'] ) ); 75 } 76 return '' === $id ? '' : hash( 'sha256', $format . "\0" . $id ); 58 77 } 59 78 -
veloimg/trunk/src/Admin/SettingsPage.php
r3728637 r3739629 17 17 use VeloWP\Conversion\WebpUploadsFolderSnapshot; 18 18 use VeloWP\Conversion\WebpManifestMassOrchestrator; 19 use VeloWP\Conversion\WebpRetry; 20 use VeloWP\Database\AvifResultsRepository; 19 21 use VeloWP\Database\FilesRepository; 20 22 use VeloWP\Database\Installer; … … 44 46 private static $uploads_folder_candidates_request_cache = array(); 45 47 private static $theme_preview_request_cache = array(); 48 private static $admin_page_configs = array(); 46 49 47 50 public static function svg_icon( string $name ) : string { … … 100 103 private static function default_settings() : array { 101 104 return array( 102 'image_strategy' => ' webp_only',105 'image_strategy' => 'avif_webp', 103 106 'delivery_method' => 'off', 104 107 'bypass_param' => 'no_veloimg', … … 154 157 $strategy = $legacy[ $strategy ]; 155 158 } 156 return array_key_exists( $strategy, self::image_strategies() ) ? $strategy : ' webp_only';159 return array_key_exists( $strategy, self::image_strategies() ) ? $strategy : 'avif_webp'; 157 160 } 158 161 … … 171 174 private static function delivery_settings_signature( array $settings ) : string { 172 175 $payload = array( 173 'image_strategy' => self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? ' webp_only' ) ),176 'image_strategy' => self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'avif_webp' ) ), 174 177 'delivery_method' => self::sanitize_delivery_method( (string) ( $settings['delivery_method'] ?? 'off' ) ), 175 178 'bypass_param' => self::sanitize_bypass_param( (string) ( $settings['bypass_param'] ?? 'no_veloimg' ) ), … … 438 441 public static function register() : void { 439 442 self::maybe_sync_settings(); 443 add_action( 'wp_ajax_velowp_admin_page', array( __CLASS__, 'ajax_admin_page' ) ); 440 444 441 445 register_setting( … … 514 518 if ( 'home' === sanitize_key( (string) ( $in['_form'] ?? '' ) ) ) { 515 519 $out = $current; 516 $out['image_strategy'] = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? ' webp_only' ) ) );520 $out['image_strategy'] = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? 'avif_webp' ) ) ); 517 521 $out['delivery_method'] = self::sanitize_delivery_method( (string) ( $in['delivery_method'] ?? ( $current['delivery_method'] ?? 'off' ) ) ); 518 522 $bp = isset( $in['bypass_param'] ) ? (string) $in['bypass_param'] : (string) ( $current['bypass_param'] ?? 'no_veloimg' ); … … 557 561 $out = array(); 558 562 559 $out['image_strategy'] = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? ' webp_only' ) ) );563 $out['image_strategy'] = self::sanitize_image_strategy( (string) ( $in['image_strategy'] ?? ( $current['image_strategy'] ?? 'avif_webp' ) ) ); 560 564 $out['delivery_method'] = self::sanitize_delivery_method( (string) ( $in['delivery_method'] ?? ( $current['delivery_method'] ?? 'off' ) ) ); 561 565 … … 609 613 } 610 614 611 public static function render_home() : void { 612 if ( ! current_user_can( self::CAPABILITY ) ) { 613 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 614 } 615 616 $logo_h_abs = VELOWP_DIR . 'assets/images/velowp-logo-horizontal.png'; 617 $has_logo_h = is_file( $logo_h_abs ); 618 $stored_settings = (array) get_option( 'velowp_settings', array() ); 619 if ( array_key_exists( 'avif_quality', $stored_settings ) ) { 620 $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 ); 621 if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) { 622 $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality; 623 } 624 if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) { 625 $stored_settings['avif_png_quality'] = $legacy_avif_quality; 626 } 627 } 628 $settings = wp_parse_args( $stored_settings, self::default_settings() ); 629 $report = Checker::check(); 630 $avif_caps = self::detect_avif_caps(); 631 $self_test_report = self::load_self_test_report(); 632 $auto_self_test = self::auto_self_test_pending( $settings ); 633 634 $image_strategies = self::image_strategies(); 635 $image_strategy = self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'webp_only' ) ); 636 $delivery_method = self::sanitize_delivery_method( (string) ( $settings['delivery_method'] ?? 'off' ) ); 637 $webp_ready = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $report['caps']['imagick_webp'] ) || ! empty( $report['caps']['gd_webp'] ) ); 638 $avif_ready = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ) ); 639 $store_writable = ! empty( $report['caps']['store_writable'] ); 640 $webp_encoders = array(); 641 $avif_encoders = array(); 642 if ( ! empty( $report['caps']['imagick_webp'] ) ) { 643 $webp_encoders[] = 'Imagick'; 644 } 645 if ( ! empty( $report['caps']['gd_webp'] ) ) { 646 $webp_encoders[] = 'GD'; 647 } 648 if ( ! empty( $avif_caps['imagick_avif'] ) ) { 649 $avif_encoders[] = 'Imagick'; 650 } 651 if ( ! empty( $avif_caps['gd_avif'] ) ) { 652 $avif_encoders[] = 'GD'; 653 } 654 655 $webp_page_url = admin_url( 'admin.php?page=' . self::WEBP_SLUG ); 656 $avif_page_url = admin_url( 'admin.php?page=' . self::AVIF_SLUG ); 657 $help_page_url = admin_url( 'admin.php?page=' . self::HELP_SLUG ); 658 $delivery_labels = array( 659 'off' => __( 'Off (no frontend changes)', 'veloimg' ), 660 'php_safe' => __( 'PHP Safe', 'veloimg' ), 661 'apache' => __( 'Apache /.htaccess', 'veloimg' ), 662 ); 663 $delivery_status = $delivery_labels[ $delivery_method ] ?? __( 'Off (no frontend changes)', 'veloimg' ); 664 665 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION; 666 $admin_js_ver = is_file( VELOWP_DIR . 'assets/js/admin.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin.js' ) : VELOWP_VERSION; 667 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION; 668 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 669 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array(), $folder_runtime_ver, true ); 670 wp_enqueue_script( 'velowp-admin', VELOWP_URL . 'assets/js/admin.js', array( 'jquery', 'velowp-folder-statistics-runtime' ), $admin_js_ver, true ); 671 wp_localize_script( 672 'velowp-admin', 673 'velowpAdmin', 674 array( 675 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 676 'selfTestNonce' => wp_create_nonce( self::SELF_TEST_NONCE_ACTION ), 677 'autoSelfTest' => $auto_self_test, 678 'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ), 679 'frontendAuditNonce' => wp_create_nonce( self::FRONTEND_AUDIT_NONCE_ACTION ), 680 'i18n' => array( 681 'ajaxMissing' => __( 'AJAX URL is missing', 'veloimg' ), 682 'selfTestLoading' => __( 'Running self-test...', 'veloimg' ), 683 'selfTestFailed' => __( 'Could not run the self-test.', 'veloimg' ), 684 'selfTestLastCheck' => __( 'Last check:', 'veloimg' ), 685 'frontendAuditLoading' => __( 'Checking the page...', 'veloimg' ), 686 'frontendAuditFailed' => __( 'Could not check the page.', 'veloimg' ), 687 'frontendAuditEnterUrl' => __( 'Enter a page URL to check.', 'veloimg' ), 688 'frontendAuditRunAgain' => __( 'Check again', 'veloimg' ), 689 'frontendAuditIssues' => __( 'Details', 'veloimg' ), 690 'frontendAuditNoIssues' => __( 'No delivery issues found.', 'veloimg' ), 691 'frontendAuditBrokenUrls' => __( 'Broken checked URLs', 'veloimg' ), 692 'runAgain' => __( 'Run again', 'veloimg' ), 693 'checkStatusSuccess' => __( 'Passed', 'veloimg' ), 694 'checkStatusWarning' => __( 'Warning', 'veloimg' ), 695 'checkStatusError' => __( 'Failed', 'veloimg' ), 696 'checkStatusInfo' => __( 'Info', 'veloimg' ), 697 ), 698 ) 699 ); 700 701 include VELOWP_DIR . 'templates/admin-home.php'; 702 } 703 704 public static function render_help() : void { 705 if ( ! current_user_can( self::CAPABILITY ) ) { 706 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 707 } 708 709 $home_page_url = admin_url( 'admin.php?page=' . self::SLUG ); 710 $webp_page_url = admin_url( 'admin.php?page=' . self::WEBP_SLUG ); 711 $avif_page_url = admin_url( 'admin.php?page=' . self::AVIF_SLUG ); 712 713 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION; 714 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 715 716 include VELOWP_DIR . 'templates/admin-help.php'; 717 } 718 719 public static function render_avif() : void { 720 if ( ! current_user_can( self::CAPABILITY ) ) { 721 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 722 } 723 724 $stored_settings = (array) get_option( 'velowp_settings', array() ); 725 if ( array_key_exists( 'avif_quality', $stored_settings ) ) { 726 $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 ); 727 if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) { 728 $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality; 729 } 730 if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) { 731 $stored_settings['avif_png_quality'] = $legacy_avif_quality; 732 } 733 } 734 $settings = wp_parse_args( $stored_settings, self::default_settings() ); 735 $report = Checker::check(); 736 $avif_caps = self::detect_avif_caps(); 737 $store = DerivativesStore::default_store(); 738 $mapper = new PathMapper( $store ); 739 $avif_jpeg_quality = self::sanitize_avif_quality( $settings['avif_jpeg_quality'] ?? ( $settings['avif_quality'] ?? 85 ) ); 740 $avif_png_quality = self::sanitize_avif_quality( $settings['avif_png_quality'] ?? ( $settings['avif_quality'] ?? 85 ) ); 741 $store_ready = ! empty( $report['caps']['store_writable'] ); 742 $encoder_ready = ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ); 743 $avif_ready = $store_ready && $encoder_ready; 744 $avif_hosting_ready = AvifMass::hosting_readiness(); 745 $encoder_labels = array(); 746 $avif_candidates = self::get_large_recent_images( $mapper ); 747 $avif_quick = array( 748 'submitted' => false, 749 'input' => '', 750 'ok' => false, 751 'message' => '', 752 'error' => '', 753 'original_rel' => '', 754 'd_expected' => array(), 755 'result' => array(), 756 'verdict' => array(), 757 ); 758 759 if ( ! empty( $avif_caps['imagick_avif'] ) ) { 760 $encoder_labels[] = 'Imagick'; 761 } 762 if ( ! empty( $avif_caps['gd_avif'] ) ) { 763 $encoder_labels[] = 'GD'; 764 } 765 766 $store_base_rel = trim( (string) $store->base_rel(), '/' ); 767 $uploads_example_rel = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'YYYY/MM/example.jpg.avif'; 768 $theme_example_rel = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'theme/theme-slug/assets/example.png.avif'; 769 $upload = wp_get_upload_dir(); 770 $uploads_basedir = (string) ( $upload['basedir'] ?? '' ); 771 $avif_folder_candidates = self::list_uploads_year_month_candidates( $uploads_basedir ); 772 $theme_asset_candidates = self::discover_theme_asset_folders(); 773 $avif_mass_status = AvifMass::status_payload(); 774 775 if ( isset( $_POST['velowp_avif_quick_nonce'] ) && check_admin_referer( 'velowp_avif_quick', 'velowp_avif_quick_nonce' ) ) { 776 $avif_quick['submitted'] = true; 777 $raw = trim( self::post_text( 'velowp_avif_quick_input' ) ); 778 $avif_quick['input'] = $raw; 779 780 $valid = $mapper->validate_original_rel( $raw ); 781 if ( empty( $valid['ok'] ) ) { 782 $avif_quick['error'] = (string) ( $valid['error'] ?? __( 'Invalid input.', 'veloimg' ) ); 783 } else { 784 $rel = (string) $valid['rel']; 785 $processor = new AvifProcessor( 786 $store, 787 $mapper, 788 array_merge( (array) $avif_caps, array( 'exif' => function_exists( 'exif_read_data' ) ) ), 789 $settings 790 ); 791 $derivative_abs = $processor->derivative_abs_from_original_rel( $rel ); 792 $avif_quick['original_rel'] = $rel; 793 $avif_quick['d_expected'] = array( 794 'derivative_rel' => $processor->derivative_rel_from_original_rel( $rel ), 795 'derivative_abs' => $derivative_abs, 796 'derivative_url' => $processor->derivative_url_from_original_rel( $rel ), 797 'exists_before' => is_file( $derivative_abs ), 798 'exists' => is_file( $derivative_abs ), 799 'valid' => is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs ), 800 ); 801 802 $res = $processor->generate_from_rel( $rel, false ); 803 $avif_quick['result'] = is_array( $res ) ? $res : array(); 804 $avif_quick['ok'] = ! empty( $res['ok'] ); 805 $avif_quick['message'] = self::avif_quick_result_message( $avif_quick['result'], $store_ready, $encoder_ready ); 806 $avif_quick['d_expected']['exists'] = is_file( $derivative_abs ); 807 $avif_quick['d_expected']['valid'] = is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs ); 808 } 809 810 $avif_quick['verdict'] = self::build_avif_quick_verdict( $avif_quick, $store_ready, $encoder_ready ); 811 } 812 813 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION; 814 $admin_avif_js_ver = is_file( VELOWP_DIR . 'assets/js/admin-avif.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin-avif.js' ) : VELOWP_VERSION; 815 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION; 816 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 817 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array(), $folder_runtime_ver, true ); 818 wp_enqueue_script( 'velowp-admin-avif', VELOWP_URL . 'assets/js/admin-avif.js', array( 'velowp-folder-statistics-runtime' ), $admin_avif_js_ver, true ); 819 wp_localize_script( 820 'velowp-admin-avif', 821 'velowpAvifAdmin', 822 array( 823 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 824 'nonce' => wp_create_nonce( AvifMass::nonce_action() ), 825 'nonceAction' => 'velowp_avif_mass_nonce', 826 'initialMassStatus' => $avif_mass_status, 827 'initialFoldersOverview' => StatisticsDisplaySnapshot::read( 'avif' ), 828 'initialGlobalSummary' => OptimizationSummary::read( 'avif' ), 829 'optimizationSummaryAction' => 'velowp_optimization_summary', 830 'folderStatisticsTitles' => array( 'uploads' => __( 'Uploads folder statistics', 'veloimg' ), 'theme' => __( 'Theme folder statistics', 'veloimg' ) ), 831 'i18n' => array( 832 'ajaxMissing' => __( 'AJAX URL missing', 'veloimg' ), 833 'chooseFolder' => __( 'Choose a folder.', 'veloimg' ), 834 'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ), 835 'loadingStats' => __( 'Loading folder information...', 'veloimg' ), 836 'loadingAll' => __( 'Loading Media Library information...', 'veloimg' ), 837 'loadingTheme' => __( 'Loading theme folder information...', 'veloimg' ), 838 'emptyFolder' => __( 'No images were found in the Media Library for this folder.', 'veloimg' ), 839 'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ), 840 'ready' => __( 'Ready.', 'veloimg' ), 841 'starting' => __( 'Starting AVIF generation...', 'veloimg' ), 842 'startingTheme' => __( 'Starting theme folder AVIF processing...', 'veloimg' ), 843 'running' => __( 'Generating AVIF...', 'veloimg' ), 844 'runningTheme' => __( 'Generating AVIF for the theme folder...', 'veloimg' ), 845 'processingFile' => __( 'Processing file:', 'veloimg' ), 846 'currentStepTime' => __( 'Current step:', 'veloimg' ), 847 'noProgressFor' => __( 'No progress for:', 'veloimg' ), 848 'stoppingDrain' => __( 'Stopping. Finishing the current work...', 'veloimg' ), 849 'secondsShort' => __( 'sec', 'veloimg' ), 850 'minutesShort' => __( 'min', 'veloimg' ), 851 'slowCurrentFile' => __( 'This file is taking longer than usual. If the server stops the request, the file will be marked as unsuccessful and generation will continue.', 'veloimg' ), 852 'timeoutLimit' => __( 'Timeout limit:', 'veloimg' ), 853 'stopping' => __( 'Stopping...', 'veloimg' ), 854 'stopped' => __( 'Stopped.', 'veloimg' ), 855 'done' => __( 'AVIF generation completed.', 'veloimg' ), 856 'doneLabel' => __( 'Generation completed', 'veloimg' ), 857 'failed' => __( 'AVIF generation failed.', 'veloimg' ), 858 'errorTitle' => __( 'Error', 'veloimg' ), 859 'hint' => __( 'Hint', 'veloimg' ), 860 'longRun' => __( 'There are many files. Generation may take a while depending on the server. You can stop and continue later without losing progress.', 'veloimg' ), 861 'unsuccessful' => __( 'Unsuccessful', 'veloimg' ), 862 'avifBiggerExplain' => __( 'Some images are marked as "Unsuccessful" because AVIF became larger than the original, so the plugin intentionally does not save it.', 'veloimg' ), 863 'avifWhatToDo' => __( 'If you want to try again, reduce the AVIF JPEG/PNG quality and run generation again.', 'veloimg' ), 864 'unsuccessfulReasonsInline' => __( 'Unsuccessful items were processed, but the derivative was not saved. Reasons:', 'veloimg' ), 865 'skippedReasonsInline' => __( 'There are "Unsuccessful" items. The main reasons are listed here:', 'veloimg' ), 866 'skippedDetailsBelow' => __( 'There are "Unsuccessful" items. See the statistics for details.', 'veloimg' ), 867 'whyNoAvif' => __( 'Why items are still without AVIF', 'veloimg' ), 868 'ajaxFailed' => __( 'AJAX request failed', 'veloimg' ), 869 'nonceRefreshFailed' => __( 'Could not refresh the security token. Reload the VeloIMG page.', 'veloimg' ), 870 'nonceExpired' => __( 'The page session expired. Refreshing the security token...', 'veloimg' ), 871 'requestTimeout' => __( 'The server took too long to respond. This usually means the hosting environment limited the request. Try again; progress is not lost.', 'veloimg' ), 872 'requestRetrying' => __( 'The server did not answer in time. Retrying; progress is not lost.', 'veloimg' ), 873 'startWaiting' => __( 'Checking whether generation started. If it did not, the selected scope will start automatically when the server is free.', 'veloimg' ), 874 'staleRun' => __( 'This AVIF run was replaced. Refresh the status before continuing.', 'veloimg' ), 875 'networkOffline' => __( 'Network connection appears to be offline.', 'veloimg' ), 876 'networkChanged' => __( 'The connection to the server was interrupted or the network changed. Try again in a few seconds.', 'veloimg' ), 877 'locked' => __( 'Another generation process is running. Wait until it finishes.', 'veloimg' ), 878 'noDataYet' => __( 'No data yet.', 'veloimg' ), 879 'processed' => __( 'Processed:', 'veloimg' ), 880 'startAvif' => __( 'Start AVIF generation', 'veloimg' ), 881 'startAll' => __( 'Start for the entire Media Library', 'veloimg' ), 882 'startFolder' => __( 'Start AVIF generation', 'veloimg' ), 883 'startTheme' => __( 'Start for the theme folder', 'veloimg' ), 884 'continueRun' => __( 'Continue', 'veloimg' ), 885 'imagesLabel' => __( 'Images', 'veloimg' ), 886 'left' => __( 'Left', 'veloimg' ), 887 'foldersEmpty' => __( 'No data.', 'veloimg' ), 888 'foldersFailed' => __( 'Could not load folder overview.', 'veloimg' ), 889 'statsFrozen' => __( 'Generation is running. Statistics will be refreshed after completion or a confirmed stop.', 'veloimg' ), 890 /* translators: 1: total images outside monthly folders, 2: processed images, 3: ready derivatives, 4: unsuccessful images, 5: remaining images. */ 891 'outsideMonthsSummary' => str_replace( 'WebP', 'AVIF', __( 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ) ), 892 'savedLine' => __( 'Savings', 'veloimg' ), 893 'active' => __( 'In progress', 'veloimg' ), 894 'paused' => __( 'Paused', 'veloimg' ), 895 'completed' => __( 'Completed', 'veloimg' ), 896 'deleting' => __( 'Deleting AVIF...', 'veloimg' ), 897 'deletingTheme'=> __( 'Deleting AVIF files for the theme folder...', 'veloimg' ), 898 'deleteFailed' => __( 'Could not delete AVIF.', 'veloimg' ), 899 'deleted' => __( 'AVIF files were deleted.', 'veloimg' ), 900 'deletedHint' => __( 'You can run generation again.', 'veloimg' ), 901 'deleteConfirm'=> __( 'Delete all AVIF files in the selected folder? Original files will not be touched.', 'veloimg' ), 902 'deleteThemeConfirm' => __( 'Delete all AVIF files for the selected theme folder? Original files will not be touched.', 'veloimg' ), 903 'deleteAllConfirm' => __( 'Delete ALL generated AVIF files across the Media Library and theme derivatives? Original files will not be touched.', 'veloimg' ), 904 'noFolderData' => __( 'No file data available.', 'veloimg' ), 905 'open' => __( 'Open', 'veloimg' ), 906 'avif' => __( 'AVIF', 'veloimg' ), 907 'file' => __( 'File', 'veloimg' ), 908 'status' => __( 'Status', 'veloimg' ), 909 'origSize' => __( 'Original', 'veloimg' ), 910 'saving' => __( 'Savings', 'veloimg' ), 911 'loadedCount' => __( 'loaded', 'veloimg' ), 912 'loadingMore' => __( 'Loading...', 'veloimg' ), 913 'scrollToLoadMore' => __( 'Scroll down to load more...', 'veloimg' ), 914 'allLoaded' => __( 'All rows are loaded.', 'veloimg' ), 915 ), 916 ) 917 ); 918 919 include VELOWP_DIR . 'templates/admin-avif.php'; 920 } 921 922 public static function render() : void { 923 if ( ! current_user_can( self::CAPABILITY ) ) { 924 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 925 } 926 927 $active_panel = 'mass'; 928 if ( isset( $_GET['settings-updated'] ) ) { 929 $active_panel = 'setup'; 930 } 931 932 $report = Checker::check(); 933 $settings = wp_parse_args( (array) get_option( 'velowp_settings', array() ), self::default_settings() ); 934 $theme_asset_candidates = self::discover_theme_asset_folders(); 935 $theme_asset_selected = isset( $settings['theme_asset_folders'] ) && is_array( $settings['theme_asset_folders'] ) ? array_values( array_unique( array_map( 'strval', (array) $settings['theme_asset_folders'] ) ) ) : array(); 936 937 $store = DerivativesStore::default_store(); 938 $mapper = new PathMapper( $store ); 939 $repo = new QueueRepository(); 940 615 private static function home_webp_check( DerivativesStore $store, PathMapper $mapper, array $report, array $settings ) : array { 616 $repo = new QueueRepository(); 941 617 $quick = array( 942 618 'submitted' => false, … … 953 629 ); 954 630 955 $candidates = self::get_large_recent_images( $mapper );956 957 631 if ( isset( $_POST['velowp_quick_nonce'] ) && check_admin_referer( 'velowp_quick', 'velowp_quick_nonce' ) ) { 958 632 $quick['submitted'] = true; 959 $active_panel = 'quick';960 633 $quick['action'] = self::post_key( 'velowp_quick_action' ); 961 634 $raw = trim( self::post_text( 'velowp_quick_input' ) ); … … 1037 710 } 1038 711 } 712 return self::remember_home_trial( 'webp', $quick ); 713 } 714 715 private static function home_avif_check( DerivativesStore $store, PathMapper $mapper, array $avif_caps, array $settings, bool $store_ready, bool $encoder_ready ) : array { 716 $avif_quick = array( 717 'submitted' => false, 718 'input' => '', 719 'ok' => false, 720 'message' => '', 721 'error' => '', 722 'original_rel' => '', 723 'd_expected' => array(), 724 'result' => array(), 725 'verdict' => array(), 726 ); 727 728 if ( isset( $_POST['velowp_avif_quick_nonce'] ) && check_admin_referer( 'velowp_avif_quick', 'velowp_avif_quick_nonce' ) ) { 729 $avif_quick['submitted'] = true; 730 $raw = trim( self::post_text( 'velowp_avif_quick_input' ) ); 731 $avif_quick['input'] = $raw; 732 733 $valid = $mapper->validate_original_rel( $raw ); 734 if ( empty( $valid['ok'] ) ) { 735 $avif_quick['error'] = (string) ( $valid['error'] ?? __( 'Invalid input.', 'veloimg' ) ); 736 } else { 737 $rel = (string) $valid['rel']; 738 $processor = new AvifProcessor( 739 $store, 740 $mapper, 741 array_merge( (array) $avif_caps, array( 'exif' => function_exists( 'exif_read_data' ) ) ), 742 $settings 743 ); 744 $derivative_abs = $processor->derivative_abs_from_original_rel( $rel ); 745 $avif_quick['original_rel'] = $rel; 746 $avif_quick['d_expected'] = array( 747 'derivative_rel' => $processor->derivative_rel_from_original_rel( $rel ), 748 'derivative_abs' => $derivative_abs, 749 'derivative_url' => $processor->derivative_url_from_original_rel( $rel ), 750 'exists_before' => is_file( $derivative_abs ), 751 'exists' => is_file( $derivative_abs ), 752 'valid' => is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs ), 753 ); 754 755 $res = $processor->generate_from_rel( $rel, false ); 756 $avif_quick['result'] = is_array( $res ) ? $res : array(); 757 $avif_quick['ok'] = ! empty( $res['ok'] ); 758 $avif_quick['message'] = self::avif_quick_result_message( $avif_quick['result'], $store_ready, $encoder_ready ); 759 $avif_quick['d_expected']['exists'] = is_file( $derivative_abs ); 760 $avif_quick['d_expected']['valid'] = is_file( $derivative_abs ) && self::is_avif_file( $derivative_abs ); 761 } 762 763 $avif_quick['verdict'] = self::build_avif_quick_verdict( $avif_quick, $store_ready, $encoder_ready ); 764 } 765 return self::remember_home_trial( 'avif', $avif_quick ); 766 } 767 768 /** Keep one last authenticated trial per format, scoped to this WordPress site. */ 769 private static function remember_home_trial( string $format, array $quick ) : array { 770 if ( ! in_array( $format, array( 'webp', 'avif' ), true ) ) { 771 return $quick; 772 } 773 $key = 'velowp_last_trial_' . $format; 774 if ( ! empty( $quick['submitted'] ) ) { 775 $snapshot = $quick; 776 // The selected result is sufficient; do not persist unrelated queue items. 777 unset( $snapshot['tick'] ); 778 update_option( $key, $snapshot, false ); 779 return $quick; 780 } 781 $stored = get_option( $key, array() ); 782 return is_array( $stored ) && ! empty( $stored['submitted'] ) 783 ? array_replace( $quick, array_intersect_key( $stored, $quick ) ) 784 : $quick; 785 } 786 787 public static function render_home() : void { 788 if ( ! current_user_can( self::CAPABILITY ) ) { 789 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 790 } 791 792 $logo_h_abs = VELOWP_DIR . 'assets/images/velowp-logo-horizontal.png'; 793 $has_logo_h = is_file( $logo_h_abs ); 794 $stored_settings = (array) get_option( 'velowp_settings', array() ); 795 if ( array_key_exists( 'avif_quality', $stored_settings ) ) { 796 $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 ); 797 if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) { 798 $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality; 799 } 800 if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) { 801 $stored_settings['avif_png_quality'] = $legacy_avif_quality; 802 } 803 } 804 $settings = wp_parse_args( $stored_settings, self::default_settings() ); 805 $report = Checker::check(); 806 $avif_caps = self::detect_avif_caps(); 807 $self_test_report = self::load_self_test_report(); 808 $auto_self_test = self::auto_self_test_pending( $settings ); 809 810 $image_strategies = self::image_strategies(); 811 $image_strategy = self::sanitize_image_strategy( (string) ( $settings['image_strategy'] ?? 'avif_webp' ) ); 812 $delivery_method = self::sanitize_delivery_method( (string) ( $settings['delivery_method'] ?? 'off' ) ); 813 $webp_ready = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $report['caps']['imagick_webp'] ) || ! empty( $report['caps']['gd_webp'] ) ); 814 $avif_ready = ! empty( $report['caps']['store_writable'] ) && ( ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ) ); 815 $store_writable = ! empty( $report['caps']['store_writable'] ); 816 $store = DerivativesStore::default_store(); 817 $mapper = new PathMapper( $store ); 818 $recent_images = self::get_large_recent_images( $mapper ); 819 $quick = self::home_webp_check( $store, $mapper, $report, $settings ); 820 $avif_quick = self::home_avif_check( $store, $mapper, $avif_caps, $settings, $store_writable, ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ) ); 821 $candidates = self::home_trial_candidates( $recent_images, $mapper, 'webp', $quick, $settings ); 822 $avif_candidates = self::home_trial_candidates( $recent_images, $mapper, 'avif', $avif_quick, $settings ); 823 $diagnostics_open = wp_verify_nonce( self::post_text( 'velowp_quick_nonce' ), 'velowp_quick' ) 824 || wp_verify_nonce( self::post_text( 'velowp_avif_quick_nonce' ), 'velowp_avif_quick' ); 825 $bypass_query = '?' . self::sanitize_bypass_param( (string) ( $settings['bypass_param'] ?? 'no_veloimg' ) ) . '=1'; 826 $webp_encoders = array(); 827 $avif_encoders = array(); 828 if ( ! empty( $report['caps']['imagick_webp'] ) ) { 829 $webp_encoders[] = 'Imagick'; 830 } 831 if ( ! empty( $report['caps']['gd_webp'] ) ) { 832 $webp_encoders[] = 'GD'; 833 } 834 if ( ! empty( $avif_caps['imagick_avif'] ) ) { 835 $avif_encoders[] = 'Imagick'; 836 } 837 if ( ! empty( $avif_caps['gd_avif'] ) ) { 838 $avif_encoders[] = 'GD'; 839 } 840 841 $webp_page_url = admin_url( 'admin.php?page=' . self::WEBP_SLUG ); 842 $avif_page_url = admin_url( 'admin.php?page=' . self::AVIF_SLUG ); 843 $help_page_url = admin_url( 'admin.php?page=' . self::HELP_SLUG ); 844 $delivery_labels = array( 845 'off' => __( 'Off (no frontend changes)', 'veloimg' ), 846 'php_safe' => __( 'PHP Safe', 'veloimg' ), 847 'apache' => __( 'Apache /.htaccess', 'veloimg' ), 848 ); 849 $delivery_status = $delivery_labels[ $delivery_method ] ?? __( 'Off (no frontend changes)', 'veloimg' ); 850 851 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION; 852 $admin_js_ver = is_file( VELOWP_DIR . 'assets/js/admin.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin.js' ) : VELOWP_VERSION; 853 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION; 854 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 855 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( 'velowp-admin-navigation' ), $folder_runtime_ver, true ); 856 wp_enqueue_script( 'velowp-admin', VELOWP_URL . 'assets/js/admin.js', array( 'jquery', 'velowp-folder-statistics-runtime' ), $admin_js_ver, true ); 857 self::localize_admin_script( 858 'velowp-admin', 859 'velowpAdmin', 860 array( 861 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 862 'selfTestNonce' => wp_create_nonce( self::SELF_TEST_NONCE_ACTION ), 863 'autoSelfTest' => $auto_self_test, 864 'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ), 865 'optimizationSummaryAction' => 'velowp_optimization_summary', 866 'homeSummaryNonces' => array( 'webp' => wp_create_nonce( self::MASS_NONCE_ACTION ), 'avif' => wp_create_nonce( AvifMass::nonce_action() ) ), 867 'frontendAuditNonce' => wp_create_nonce( self::FRONTEND_AUDIT_NONCE_ACTION ), 868 'i18n' => array( 869 'selectImage' => __( 'Select an image', 'veloimg' ), 870 'ajaxMissing' => __( 'AJAX URL is missing', 'veloimg' ), 871 'selfTestLoading' => __( 'Running self-test...', 'veloimg' ), 872 'selfTestFailed' => __( 'Could not run the self-test.', 'veloimg' ), 873 'selfTestLastCheck' => __( 'Last check:', 'veloimg' ), 874 'frontendAuditLoading' => __( 'Checking the page...', 'veloimg' ), 875 'frontendAuditFailed' => __( 'Could not check the page.', 'veloimg' ), 876 'frontendAuditEnterUrl' => __( 'Enter a page URL to check.', 'veloimg' ), 877 'frontendAuditRunAgain' => __( 'Check again', 'veloimg' ), 878 'frontendAuditIssues' => __( 'Details', 'veloimg' ), 879 'frontendAuditNoIssues' => __( 'No delivery issues found.', 'veloimg' ), 880 'frontendAuditBrokenUrls' => __( 'Broken checked URLs', 'veloimg' ), 881 'runAgain' => __( 'Run again', 'veloimg' ), 882 'checkStatusSuccess' => __( 'Passed', 'veloimg' ), 883 'checkStatusWarning' => __( 'Warning', 'veloimg' ), 884 'checkStatusError' => __( 'Failed', 'veloimg' ), 885 'checkStatusInfo' => __( 'Info', 'veloimg' ), 886 ), 887 ) 888 ); 889 890 include VELOWP_DIR . 'templates/admin-home.php'; 891 self::render_navigation_config( self::SLUG ); 892 } 893 894 public static function render_help() : void { 895 if ( ! current_user_can( self::CAPABILITY ) ) { 896 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 897 } 898 899 $home_page_url = admin_url( 'admin.php?page=' . self::SLUG ); 900 $webp_page_url = admin_url( 'admin.php?page=' . self::WEBP_SLUG ); 901 $avif_page_url = admin_url( 'admin.php?page=' . self::AVIF_SLUG ); 902 903 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION; 904 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 905 906 include VELOWP_DIR . 'templates/admin-help.php'; 907 self::render_navigation_config( self::HELP_SLUG ); 908 } 909 910 public static function render_avif() : void { 911 if ( ! current_user_can( self::CAPABILITY ) ) { 912 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 913 } 914 915 $stored_settings = (array) get_option( 'velowp_settings', array() ); 916 if ( array_key_exists( 'avif_quality', $stored_settings ) ) { 917 $legacy_avif_quality = self::sanitize_avif_quality( $stored_settings['avif_quality'] ?? 85 ); 918 if ( ! array_key_exists( 'avif_jpeg_quality', $stored_settings ) ) { 919 $stored_settings['avif_jpeg_quality'] = $legacy_avif_quality; 920 } 921 if ( ! array_key_exists( 'avif_png_quality', $stored_settings ) ) { 922 $stored_settings['avif_png_quality'] = $legacy_avif_quality; 923 } 924 } 925 $settings = wp_parse_args( $stored_settings, self::default_settings() ); 926 $report = Checker::check(); 927 $avif_caps = self::detect_avif_caps(); 928 $store = DerivativesStore::default_store(); 929 $mapper = new PathMapper( $store ); 930 $avif_jpeg_quality = self::sanitize_avif_quality( $settings['avif_jpeg_quality'] ?? ( $settings['avif_quality'] ?? 85 ) ); 931 $avif_png_quality = self::sanitize_avif_quality( $settings['avif_png_quality'] ?? ( $settings['avif_quality'] ?? 85 ) ); 932 $store_ready = ! empty( $report['caps']['store_writable'] ); 933 $encoder_ready = ! empty( $avif_caps['imagick_avif'] ) || ! empty( $avif_caps['gd_avif'] ); 934 $avif_ready = $store_ready && $encoder_ready; 935 $avif_hosting_ready = AvifMass::hosting_readiness(); 936 $encoder_labels = array(); 937 938 if ( ! empty( $avif_caps['imagick_avif'] ) ) { 939 $encoder_labels[] = 'Imagick'; 940 } 941 if ( ! empty( $avif_caps['gd_avif'] ) ) { 942 $encoder_labels[] = 'GD'; 943 } 944 945 $store_base_rel = trim( (string) $store->base_rel(), '/' ); 946 $uploads_example_rel = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'YYYY/MM/example.jpg.avif'; 947 $theme_example_rel = ( '' !== $store_base_rel ? $store_base_rel . '/' : '' ) . 'theme/theme-slug/assets/example.png.avif'; 948 $upload = wp_get_upload_dir(); 949 $uploads_basedir = (string) ( $upload['basedir'] ?? '' ); 950 $avif_folder_candidates = self::list_uploads_year_month_candidates( $uploads_basedir ); 951 $theme_asset_candidates = self::discover_theme_asset_folders(); 952 $avif_mass_status = AvifMass::status_payload(); 953 954 955 $admin_base_css_ver = is_file( VELOWP_DIR . 'assets/css/admin-base.css' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/css/admin-base.css' ) : VELOWP_VERSION; 956 $admin_avif_js_ver = is_file( VELOWP_DIR . 'assets/js/admin-avif.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/admin-avif.js' ) : VELOWP_VERSION; 957 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION; 958 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 959 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( 'velowp-admin-navigation' ), $folder_runtime_ver, true ); 960 wp_enqueue_script( 'velowp-admin-avif', VELOWP_URL . 'assets/js/admin-avif.js', array( 'velowp-folder-statistics-runtime' ), $admin_avif_js_ver, true ); 961 self::localize_admin_script( 962 'velowp-admin-avif', 963 'velowpAvifAdmin', 964 array( 965 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 966 'nonce' => wp_create_nonce( AvifMass::nonce_action() ), 967 'nonceAction' => 'velowp_avif_mass_nonce', 968 'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ), 969 'initialMassStatus' => $avif_mass_status, 970 'initialFoldersOverview' => StatisticsDisplaySnapshot::read( 'avif' ), 971 'initialGlobalSummary' => OptimizationSummary::read( 'avif' ), 972 'optimizationSummaryAction' => 'velowp_optimization_summary', 973 'folderStatisticsTitles' => array( 'uploads' => __( 'Uploads folder statistics', 'veloimg' ), 'theme' => __( 'Theme folder statistics', 'veloimg' ) ), 974 'i18n' => array( 975 'ajaxMissing' => __( 'AJAX URL missing', 'veloimg' ), 976 'chooseFolder' => __( 'Choose a folder.', 'veloimg' ), 977 'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ), 978 'loadingStats' => __( 'Loading folder information...', 'veloimg' ), 979 'loadingAll' => __( 'Loading uploads information...', 'veloimg' ), 980 'loadingTheme' => __( 'Loading theme folder information...', 'veloimg' ), 981 'emptyFolder' => __( 'No images were found in uploads for this folder.', 'veloimg' ), 982 'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ), 983 'ready' => __( 'Ready.', 'veloimg' ), 984 'starting' => __( 'Starting AVIF generation...', 'veloimg' ), 985 'startingTheme' => __( 'Starting theme folder AVIF processing...', 'veloimg' ), 986 'running' => __( 'Generating AVIF...', 'veloimg' ), 987 'runningTheme' => __( 'Generating AVIF for the theme folder...', 'veloimg' ), 988 'processingFile' => __( 'Processing file:', 'veloimg' ), 989 'currentStepTime' => __( 'Current step:', 'veloimg' ), 990 'noProgressFor' => __( 'No progress for:', 'veloimg' ), 991 'stoppingDrain' => __( 'Stopping. Finishing the current work...', 'veloimg' ), 992 'secondsShort' => __( 'sec', 'veloimg' ), 993 'minutesShort' => __( 'min', 'veloimg' ), 994 'slowCurrentFile' => __( 'This file is taking longer than usual. If the server stops the request, the file will be marked as unsuccessful and generation will continue.', 'veloimg' ), 995 'timeoutLimit' => __( 'Timeout limit:', 'veloimg' ), 996 'stopping' => __( 'Stopping...', 'veloimg' ), 997 'stopped' => __( 'Stopped.', 'veloimg' ), 998 'done' => __( 'AVIF generation completed.', 'veloimg' ), 999 'doneLabel' => __( 'Generation completed', 'veloimg' ), 1000 'failed' => __( 'AVIF generation failed.', 'veloimg' ), 1001 'errorTitle' => __( 'Error', 'veloimg' ), 1002 'hint' => __( 'Hint', 'veloimg' ), 1003 'longRun' => __( 'There are many files. Generation may take a while depending on the server. You can stop and continue later without losing progress.', 'veloimg' ), 1004 'unsuccessful' => __( 'Unsuccessful', 'veloimg' ), 1005 'avifBiggerExplain' => __( 'Some images are marked as "Unsuccessful" because AVIF became larger than the original, so the plugin intentionally does not save it.', 'veloimg' ), 1006 'avifWhatToDo' => __( 'If you want to try again, reduce the AVIF JPEG/PNG quality and run generation again.', 'veloimg' ), 1007 'unsuccessfulReasonsInline' => __( 'Unsuccessful items were processed, but the derivative was not saved. Reasons:', 'veloimg' ), 1008 'skippedReasonsInline' => __( 'There are "Unsuccessful" items. The main reasons are listed here:', 'veloimg' ), 1009 'skippedDetailsBelow' => __( 'There are "Unsuccessful" items. See the statistics for details.', 'veloimg' ), 1010 'whyNoAvif' => __( 'Why items are still without AVIF', 'veloimg' ), 1011 'ajaxFailed' => __( 'AJAX request failed', 'veloimg' ), 1012 'nonceRefreshFailed' => __( 'Could not refresh the security token. Reload the VeloIMG page.', 'veloimg' ), 1013 'nonceExpired' => __( 'The page session expired. Refreshing the security token...', 'veloimg' ), 1014 'requestTimeout' => __( 'The server took too long to respond. This usually means the hosting environment limited the request. Try again; progress is not lost.', 'veloimg' ), 1015 'requestRetrying' => __( 'The server did not answer in time. Retrying; progress is not lost.', 'veloimg' ), 1016 'startWaiting' => __( 'Checking whether generation started. If it did not, the selected scope will start automatically when the server is free.', 'veloimg' ), 1017 'staleRun' => __( 'This AVIF run was replaced. Refresh the status before continuing.', 'veloimg' ), 1018 'networkOffline' => __( 'Network connection appears to be offline.', 'veloimg' ), 1019 'networkChanged' => __( 'The connection to the server was interrupted or the network changed. Try again in a few seconds.', 'veloimg' ), 1020 'locked' => __( 'Another generation process is running. Wait until it finishes.', 'veloimg' ), 1021 'noDataYet' => __( 'No data yet.', 'veloimg' ), 1022 'processed' => __( 'Processed:', 'veloimg' ), 1023 'startAvif' => __( 'Start AVIF generation', 'veloimg' ), 1024 'startAll' => __( 'Start for all uploads', 'veloimg' ), 1025 'startFolder' => __( 'Start AVIF generation', 'veloimg' ), 1026 'startTheme' => __( 'Start for the theme folder', 'veloimg' ), 1027 'continueRun' => __( 'Continue', 'veloimg' ), 1028 'imagesLabel' => __( 'Images', 'veloimg' ), 1029 'left' => __( 'Left', 'veloimg' ), 1030 'foldersEmpty' => __( 'No data.', 'veloimg' ), 1031 'foldersFailed' => __( 'Could not load folder overview.', 'veloimg' ), 1032 'statsFrozen' => __( 'Generation is running. Statistics will be refreshed after completion or a confirmed stop.', 'veloimg' ), 1033 /* translators: 1: total images outside monthly folders, 2: processed images, 3: ready derivatives, 4: unsuccessful images, 5: remaining images. */ 1034 'outsideMonthsSummary' => str_replace( 'WebP', 'AVIF', __( 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ) ), 1035 'savedLine' => __( 'Savings', 'veloimg' ), 1036 'active' => __( 'In progress', 'veloimg' ), 1037 'paused' => __( 'Paused', 'veloimg' ), 1038 'completed' => __( 'Completed', 'veloimg' ), 1039 'deleting' => __( 'Deleting AVIF...', 'veloimg' ), 1040 'deletingTheme'=> __( 'Deleting AVIF files for the theme folder...', 'veloimg' ), 1041 'deleteFailed' => __( 'Could not delete AVIF.', 'veloimg' ), 1042 'deleted' => __( 'AVIF files were deleted.', 'veloimg' ), 1043 'deletedHint' => __( 'You can run generation again.', 'veloimg' ), 1044 'deleteConfirm'=> __( 'Delete all AVIF files in the selected folder? Original files will not be touched.', 'veloimg' ), 1045 'deleteThemeConfirm' => __( 'Delete all AVIF files for the selected theme folder? Original files will not be touched.', 'veloimg' ), 1046 'deleteAllConfirm' => __( 'Delete ALL generated AVIF files across the Media Library and theme derivatives? Original files will not be touched.', 'veloimg' ), 1047 'noFolderData' => __( 'No file data available.', 'veloimg' ), 1048 'open' => __( 'Open', 'veloimg' ), 1049 'avif' => __( 'AVIF', 'veloimg' ), 1050 'file' => __( 'File', 'veloimg' ), 1051 'status' => __( 'Status', 'veloimg' ), 1052 'origSize' => __( 'Original', 'veloimg' ), 1053 'saving' => __( 'Savings', 'veloimg' ), 1054 'loadedCount' => __( 'loaded', 'veloimg' ), 1055 'loadingMore' => __( 'Loading...', 'veloimg' ), 1056 'scrollToLoadMore' => __( 'Scroll down to load more...', 'veloimg' ), 1057 'allLoaded' => __( 'All rows are loaded.', 'veloimg' ), 1058 ), 1059 ) 1060 ); 1061 1062 include VELOWP_DIR . 'templates/admin-avif.php'; 1063 self::render_navigation_config( self::AVIF_SLUG ); 1064 } 1065 1066 public static function render() : void { 1067 if ( ! current_user_can( self::CAPABILITY ) ) { 1068 wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'veloimg' ) ); 1069 } 1070 1071 $active_panel = 'mass'; 1072 if ( isset( $_GET['settings-updated'] ) ) { 1073 $active_panel = 'setup'; 1074 } 1075 1076 $report = Checker::check(); 1077 $settings = wp_parse_args( (array) get_option( 'velowp_settings', array() ), self::default_settings() ); 1078 $theme_asset_candidates = self::discover_theme_asset_folders(); 1079 $theme_asset_selected = isset( $settings['theme_asset_folders'] ) && is_array( $settings['theme_asset_folders'] ) ? array_values( array_unique( array_map( 'strval', (array) $settings['theme_asset_folders'] ) ) ) : array(); 1080 1081 $store = DerivativesStore::default_store(); 1082 $mapper = new PathMapper( $store ); 1083 $repo = new QueueRepository(); 1084 1039 1085 1040 1086 $scan = array( … … 1174 1220 $folder_runtime_ver = is_file( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) ? (string) (int) filemtime( VELOWP_DIR . 'assets/js/folder-statistics-runtime.js' ) : VELOWP_VERSION; 1175 1221 wp_enqueue_style( 'velowp-admin-base', VELOWP_URL . 'assets/css/admin-base.css', array(), $admin_base_css_ver ); 1176 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( ), $folder_runtime_ver, true );1222 wp_enqueue_script( 'velowp-folder-statistics-runtime', VELOWP_URL . 'assets/js/folder-statistics-runtime.js', array( 'velowp-admin-navigation' ), $folder_runtime_ver, true ); 1177 1223 wp_enqueue_script( 'velowp-admin', VELOWP_URL . 'assets/js/admin.js', array( 'jquery', 'velowp-folder-statistics-runtime' ), $admin_js_ver, true ); 1178 wp_localize_script(1224 self::localize_admin_script( 1179 1225 'velowp-admin', 1180 1226 'velowpAdmin', … … 1182 1228 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 1183 1229 'nonce' => wp_create_nonce( self::MASS_NONCE_ACTION ), 1230 'homeStatusNonce' => wp_create_nonce( 'velowp_home_status' ), 1184 1231 'initialFoldersOverview' => StatisticsDisplaySnapshot::read( 'webp' ), 1185 1232 'initialGlobalSummary' => OptimizationSummary::read( 'webp' ), … … 1193 1240 'unableStats' => __( 'Could not load folder information', 'veloimg' ), 1194 1241 'chooseFolder' => __( 'Choose a folder.', 'veloimg' ), 1195 'emptyFolder' => __( 'No images were found in the Media Libraryfor this folder.', 'veloimg' ),1242 'emptyFolder' => __( 'No images were found in uploads for this folder.', 'veloimg' ), 1196 1243 'nothingToDo' => __( 'Everything in this folder is already optimized.', 'veloimg' ), 1197 1244 'ready' => __( 'Ready.', 'veloimg' ), … … 1235 1282 'chooseFolder' => __( 'Choose a folder.', 'veloimg' ), 1236 1283 'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ), 1237 'emptyFolder' => __( 'No images were found in the Media Libraryfor this folder.', 'veloimg' ),1284 'emptyFolder' => __( 'No images were found in uploads for this folder.', 'veloimg' ), 1238 1285 'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ), 1239 1286 'nothingToDo' => __( 'Everything in this folder is already optimized.', 'veloimg' ), … … 1249 1296 'doneLabel' => __( 'Generation completed', 'veloimg' ), 1250 1297 'continueRun' => __( 'Continue', 'veloimg' ), 1251 'startAll' => __( 'Start for the entire Media Library', 'veloimg' ),1298 'startAll' => __( 'Start for all uploads', 'veloimg' ), 1252 1299 'startFolder' => __( 'Start generation', 'veloimg' ), 1253 1300 'startFailed' => __( 'Could not start', 'veloimg' ), 1254 1301 'startTheme' => __( 'Start for the theme folder', 'veloimg' ), 1255 1302 'stepFailed' => __( 'Step failed', 'veloimg' ), 1256 'loadingAll' => __( 'Loading Media Libraryinformation...', 'veloimg' ),1303 'loadingAll' => __( 'Loading uploads information...', 'veloimg' ), 1257 1304 'noDataYet' => __( 'No data yet.', 'veloimg' ), 1258 1305 'noFolderData' => __( 'No file data available.', 'veloimg' ), … … 1294 1341 'networkChanged' => __( 'The connection to the server was interrupted or the network changed. Try again in a few seconds.', 'veloimg' ), 1295 1342 'loadingStats' => __( 'Loading folder information...', 'veloimg' ), 1343 /* translators: %d: number of unsuccessful files eligible for retry. */ 1344 'retryCount' => __( 'Files eligible for retry in this selection: %d', 'veloimg' ), 1345 /* translators: %d: number of unsuccessful files selected for retry. */ 1346 'retryStart' => __( 'Retry processing %d files', 'veloimg' ), 1347 'retryRunning' => __( 'Retry processing is running. Progress and Stop are in the generation block above.', 'veloimg' ), 1348 'retryUnavailable' => __( 'Retry processing will be available after statistics are updated and generation is stopped or completed.', 'veloimg' ), 1296 1349 'loadingTheme' => __( 'Loading theme folder information...', 'veloimg' ), 1297 1350 'unableStats' => __( 'Could not load folder information', 'veloimg' ), 1298 1351 'chooseFolder' => __( 'Choose a folder.', 'veloimg' ), 1299 1352 'chooseThemeFolder' => __( 'Choose a theme folder.', 'veloimg' ), 1300 'emptyFolder' => __( 'No images were found in the Media Libraryfor this folder.', 'veloimg' ),1353 'emptyFolder' => __( 'No images were found in uploads for this folder.', 'veloimg' ), 1301 1354 'emptyThemeFolder' => __( 'No JPG/PNG images were found in the selected theme folder.', 'veloimg' ), 1302 1355 'nothingToDo' => __( 'Everything in this folder is already optimized.', 'veloimg' ), … … 1317 1370 'doneLabel' => __( 'Generation completed', 'veloimg' ), 1318 1371 'continueRun' => __( 'Continue', 'veloimg' ), 1319 'startAll' => __( 'Start for the entire Media Library', 'veloimg' ),1372 'startAll' => __( 'Start for all uploads', 'veloimg' ), 1320 1373 'startFolder' => __( 'Start generation', 'veloimg' ), 1321 1374 'startFailed' => __( 'Could not start', 'veloimg' ), 1322 1375 'startTheme' => __( 'Start for the theme folder', 'veloimg' ), 1323 1376 'stepFailed' => __( 'Step failed', 'veloimg' ), 1324 'loadingAll' => __( 'Loading Media Libraryinformation...', 'veloimg' ),1377 'loadingAll' => __( 'Loading uploads information...', 'veloimg' ), 1325 1378 'noDataYet' => __( 'No data yet.', 'veloimg' ), 1326 1379 'noFolderData' => __( 'No file data available.', 'veloimg' ), … … 1363 1416 'queuePendingOne' => __( 'In queue', 'veloimg' ), 1364 1417 /* translators: 1: total images outside monthly folders, 2: processed images, 3: ready derivatives, 4: unsuccessful images, 5: remaining images. */ 1365 'outsideMonthsSummary' => __( 'Outside monthly folders (included in Entire Media Library): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ),1418 'outsideMonthsSummary' => __( 'Outside monthly folders (included in All uploads): %1$d images; processed: %2$d; WebP: %3$d; unsuccessful: %4$d; left: %5$d.', 'veloimg' ), 1366 1419 'queueProcessing' => __( 'Processing', 'veloimg' ), 1367 1420 'queueProcessingOne' => __( 'Processing', 'veloimg' ), … … 1441 1494 wp_enqueue_media(); 1442 1495 1496 $admin_i18n_overrides['i18n'] = array_replace( self::$admin_page_configs['velowpAdmin']['i18n'], $admin_i18n_overrides['i18n'] ); 1497 self::$admin_page_configs['velowpAdmin'] = array_replace( self::$admin_page_configs['velowpAdmin'], $admin_i18n_overrides ); 1443 1498 include VELOWP_DIR . 'templates/admin-dashboard.php'; 1499 self::render_navigation_config( self::WEBP_SLUG ); 1444 1500 return; 1445 1501 } 1446 1502 1503 /** Keep the native page and its AJAX fragment on the same configuration path. */ 1504 private static function localize_admin_script( string $handle, string $object, array $data ) : void { 1505 self::$admin_page_configs[ $object ] = $data; 1506 wp_localize_script( $handle, $object, $data ); 1507 } 1508 1509 private static function render_navigation_config( string $page ) : void { 1510 $pages = array( self::SLUG, self::WEBP_SLUG, self::AVIF_SLUG, self::HELP_SLUG ); 1511 $labels = array( 1512 self::SLUG => __( 'VeloIMG', 'veloimg' ), 1513 self::WEBP_SLUG => __( 'WebP generation', 'veloimg' ), 1514 self::AVIF_SLUG => __( 'AVIF generation', 'veloimg' ), 1515 self::HELP_SLUG => __( 'Help / FAQ', 'veloimg' ), 1516 ); 1517 foreach ( array( 'admin-navigation', 'folder-statistics-runtime', 'admin', 'admin-avif' ) as $asset ) { 1518 $file = 'assets/js/' . $asset . '.js'; 1519 $version = is_file( VELOWP_DIR . $file ) ? (string) (int) filemtime( VELOWP_DIR . $file ) : VELOWP_VERSION; 1520 $deps = 'admin-navigation' === $asset ? array( 'jquery' ) : array( 'jquery', 'velowp-admin-navigation' ); 1521 if ( in_array( $asset, array( 'admin', 'admin-avif' ), true ) ) { $deps[] = 'velowp-folder-statistics-runtime'; } 1522 wp_enqueue_script( 'velowp-' . $asset, VELOWP_URL . $file, $deps, $version, true ); 1523 } 1524 // The media picker is available after any internal transition, not just a native WebP load. 1525 wp_enqueue_media(); 1526 $packet = array( 1527 'page' => $page, 1528 'title' => $labels[ $page ] . ' ‹ VeloIMG — ' . get_bloginfo( 'name' ), 1529 'admin' => self::$admin_page_configs['velowpAdmin'] ?? array(), 1530 'avif' => self::$admin_page_configs['velowpAvifAdmin'] ?? array(), 1531 'navigation' => array( 1532 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 1533 'pageUrl' => admin_url( 'admin.php' ), 1534 'pages' => $pages, 1535 'nonce' => wp_create_nonce( 'velowp_admin_navigation' ), 1536 'i18n' => array( 1537 'loading' => __( 'Loading section...', 'veloimg' ), 1538 'saving' => __( 'Sending request...', 'veloimg' ), 1539 'failed' => __( 'Could not load the section. The current page is still available.', 'veloimg' ), 1540 'saveFailed' => __( 'The server response could not be confirmed. Open the page to check the result before trying again.', 'veloimg' ), 1541 'openPage' => __( 'Open page', 'veloimg' ), 1542 'unsaved' => __( 'There are unsaved settings. Leave this section without saving?', 'veloimg' ), 1543 ), 1544 ), 1545 ); 1546 // JSON is inert page data, never executable inline code. 1547 echo '<script type="application/json" id="velowp-admin-page-config">' . wp_json_encode( $packet, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON_HEX_* encodes HTML metacharacters in this inert JSON data block. 1548 } 1549 1550 public static function ajax_admin_page() : void { 1551 if ( ! current_user_can( self::CAPABILITY ) || ! check_ajax_referer( 'velowp_admin_navigation', 'nonce', false ) ) { 1552 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'veloimg' ) ), 403 ); 1553 } 1554 $page = isset( $_POST['page'] ) ? sanitize_key( wp_unslash( $_POST['page'] ) ) : ''; 1555 $renderers = array( self::SLUG => 'render_home', self::WEBP_SLUG => 'render', self::AVIF_SLUG => 'render_avif', self::HELP_SLUG => 'render_help' ); 1556 if ( ! isset( $renderers[ $page ] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid request.', 'veloimg' ) ), 400 ); } 1557 $request_post = $_POST; 1558 $request_get = $_GET; 1559 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; 1560 // This endpoint only renders. Native trial/save POSTs never enter this path. 1561 $_POST = array(); 1562 $_GET = array( 'page' => $page ); 1563 if ( ! empty( $request_post['settings_updated'] ) ) { $_GET['settings-updated'] = '1'; } 1564 // wp_nonce_field()/Settings API must refer back to the real page, not admin-ajax.php. 1565 $_SERVER['REQUEST_URI'] = (string) wp_parse_url( admin_url( 'admin.php' ), PHP_URL_PATH ) . '?' . http_build_query( $_GET ); 1566 ob_start(); 1567 try { self::{ $renderers[ $page ] }(); $html = ob_get_contents(); } 1568 finally { ob_end_clean(); $_POST = $request_post; $_GET = $request_get; $_SERVER['REQUEST_URI'] = $request_uri; } 1569 wp_send_json_success( array( 'html' => $html ) ); 1570 } 1447 1571 1448 1572 private static function find_quick_tick_item( array $tick, string $original_rel ) : array { … … 1832 1956 ); 1833 1957 1834 return array_slice( $items, 0, 10 ); 1958 return $items; 1959 } 1960 1961 /** Read existing evidence only: never convert images while building the picker. */ 1962 private static function home_trial_candidates( array $items, PathMapper $mapper, string $format, array $quick, array $settings ) : array { 1963 $paths = array_column( $items, 'rel' ); 1964 $is_avif = 'avif' === $format; 1965 $records = $is_avif 1966 ? ( new AvifResultsRepository() )->map_by_source_paths( 'uploads', $paths ) 1967 : ( new WebpResultsRepository() )->map_by_source_paths( 'uploads', 'uploads', $paths ); 1968 $queue = $is_avif ? array() : self::queue_map_for_originals( $paths ); 1969 $avif_processor = $is_avif ? new AvifProcessor( DerivativesStore::default_store(), $mapper, array(), $settings ) : null; 1970 $ready = array(); 1971 $untested_jpeg = array(); 1972 $untested_png = array(); 1973 foreach ( $items as $item ) { 1974 $rel = (string) ( $item['rel'] ?? '' ); 1975 $src_abs = $mapper->original_abs_from_original_rel( $rel ); 1976 if ( ! is_file( $src_abs ) || is_link( $src_abs ) ) { 1977 continue; 1978 } 1979 $src_mtime = (int) filemtime( $src_abs ); 1980 $src_bytes = (int) filesize( $src_abs ); 1981 $row = (array) ( $records[ $rel ] ?? array() ); 1982 $last_trial = $rel === (string) ( $quick['original_rel'] ?? '' ) && ! empty( $quick['verdict']['done'] ); 1983 $last_passed = $last_trial && ! empty( $quick['verdict']['passed'] ); 1984 $dst_abs = $is_avif ? $avif_processor->derivative_abs_from_original_rel( $rel ) : $mapper->derivative_abs_from_original_rel( $rel ); 1985 $is_png = 'png' === strtolower( (string) pathinfo( $rel, PATHINFO_EXTENSION ) ); 1986 $usable = is_file( $dst_abs ) && ! is_link( $dst_abs ) 1987 && (int) filesize( $dst_abs ) > 0 && (int) filesize( $dst_abs ) <= $src_bytes 1988 && (int) filemtime( $dst_abs ) >= $src_mtime 1989 && ( $is_avif ? self::is_avif_file( $dst_abs ) : self::is_webp_file( $dst_abs ) ); 1990 // PNG/AVIF needs previous successful validation, not just a container header. 1991 if ( $usable && ( ! $is_avif || ! $is_png || $last_passed || 'done' === (string) ( $row['status'] ?? '' ) ) ) { 1992 $ready[] = $item; 1993 if ( count( $ready ) >= 10 ) { 1994 break; 1995 } 1996 continue; 1997 } 1998 $record_current = $is_avif 1999 ? (int) ( $row['src_bytes'] ?? 0 ) === $src_bytes 2000 : ( (int) ( $row['original_mtime'] ?? -1 ) === $src_mtime && (int) ( $row['src_bytes'] ?? -1 ) === $src_bytes ); 2001 $queue_status = (string) ( $queue[ $rel ]['status'] ?? '' ); 2002 $queue_failed = 'error' === $queue_status || ( 'skipped' === $queue_status && 'up_to_date' !== (string) ( $queue[ $rel ]['last_error'] ?? '' ) ); 2003 if ( ( $record_current && in_array( (string) ( $row['status'] ?? '' ), array( 'error', 'skipped' ), true ) ) 2004 || $queue_failed 2005 || ( $last_trial && ! $last_passed ) ) { 2006 continue; 2007 } 2008 if ( $is_png ) { 2009 $untested_png[] = $item; 2010 } else { 2011 $untested_jpeg[] = $item; 2012 } 2013 } 2014 return array_slice( array_merge( $ready, $untested_jpeg, $untested_png ), 0, 10 ); 1835 2015 } 1836 2016 … … 2307 2487 ? __( 'Another run control request is in progress. Wait briefly and try again.', 'veloimg' ) 2308 2488 : __( 'The run state could not be confirmed. Synchronize the page before retrying.', 'veloimg' ); 2489 if ( 'retry_quality_invalid' === $code ) { $message = __( 'Choose a retry quality between 50 and 100.', 'veloimg' ); } 2490 if ( 'retry_engine_unavailable' === $code ) { $message = __( 'The selected WebP encoder is unavailable. Refresh the page and select an available encoder.', 'veloimg' ); } 2309 2491 $result = array( 'ok' => false, 'code' => $code, 'message' => $message, 'retry_after' => 2 ); 2310 2492 try { $result['state'] = WebpRunState::state(); } catch ( WebpStateException $read_error ) { $result['state_unavailable'] = true; } … … 3924 4106 $all_original_bytes = 0; 3925 4107 $last_activity_ts = 0; 4108 $retry_paths = array(); 3926 4109 foreach ( $theme_files as $status ) { 3927 4110 $all_original_bytes += max( 0, (int) ( $status['src_bytes'] ?? 0 ) ); … … 3931 4114 $result_status = (string) ( $result['status'] ?? '' ); 3932 4115 $is_unsuccessful = ! $is_ready && in_array( $result_status, array( 'skipped', 'error' ), true ); 4116 if ( $is_unsuccessful && WebpRetry::candidate( trailingslashit( (string) $status['root_dir'] ) . (string) $status['rel'], (string) ( $result['reason'] ?? '' ) ) ) { 4117 $retry_paths[] = (string) $status['root_slug'] . '/' . (string) $status['rel']; 4118 } 3933 4119 3934 4120 if ( $is_ready ) { … … 3986 4172 } 3987 4173 4174 $snapshot['retryable'] = count( $retry_paths ); 3988 4175 return self::$theme_preview_request_cache[ $cache_key ] = array( 4176 'retry_paths' => $retry_paths, 3989 4177 'files' => $files, 3990 4178 'reasons' => $reasons, … … 4181 4369 $summary = array( 4182 4370 'total' => $total, 4371 'retryable' => (int) ( $theme_snapshot['retryable'] ?? 0 ), 4183 4372 'success' => $done, 4184 4373 'failed' => $failed, … … 4291 4480 $locked_until = ! empty( $lock['locked'] ) ? (int) ( $lock['until'] ?? 0 ) : 0; 4292 4481 $now_ts = (int) current_time( 'timestamp' ); 4293 $folder_label = ( '__all__' === $folder_rel ) ? __( ' Entire Media Library', 'veloimg' ) : $folder_rel;4482 $folder_label = ( '__all__' === $folder_rel ) ? __( 'All uploads', 'veloimg' ) : $folder_rel; 4294 4483 4295 4484 $mass_state = WebpRunState::state(); … … 4300 4489 $summary = array( 4301 4490 'total' => $total, 4491 'retryable' => (int) ( $physical['retryable'] ?? 0 ), 4302 4492 'success' => $done, 4303 4493 'failed' => $failed, … … 5516 5706 'queue' => $queue, 5517 5707 'skipped_reasons' => self::build_failed_reason_rows( (array) $physical['reason_counts'] ), 5518 'summary' => array( 'total' => $total, ' success' => $success, 'failed' => $failed, 'processed' => (int) ( $physical['processed'] ?? 0 ), 'remaining' => (int) ( $physical['remaining'] ?? 0 ), 'pending' => (int) ( $physical['pending'] ?? 0 ), 'processing' => (int) ( $physical['processing'] ?? 0 ), 'failed_reasons' => self::build_failed_reason_rows( (array) $physical['reason_counts'] ), 'outside_months' => (int) ( $all_physical['outside_months'] ?? 0 ), 'outside_summary' => (array) ( $all_physical['outside_summary'] ?? array() ), 'jpg' => (int) ( $physical['jpg'] ?? 0 ), 'png' => (int) ( $physical['png'] ?? 0 ) ),5708 'summary' => array( 'total' => $total, 'retryable' => (int) ( $physical['retryable'] ?? 0 ), 'success' => $success, 'failed' => $failed, 'processed' => (int) ( $physical['processed'] ?? 0 ), 'remaining' => (int) ( $physical['remaining'] ?? 0 ), 'pending' => (int) ( $physical['pending'] ?? 0 ), 'processing' => (int) ( $physical['processing'] ?? 0 ), 'failed_reasons' => self::build_failed_reason_rows( (array) $physical['reason_counts'] ), 'outside_months' => (int) ( $all_physical['outside_months'] ?? 0 ), 'outside_summary' => (array) ( $all_physical['outside_summary'] ?? array() ), 'jpg' => (int) ( $physical['jpg'] ?? 0 ), 'png' => (int) ( $physical['png'] ?? 0 ) ), 5519 5709 'global_summary' => self::global_optimization_summary( $all_physical ), 5520 5710 'heartbeat' => $heartbeat, … … 5545 5735 $folder_rel = (string) $reservation['folder']; 5546 5736 if ( 'theme' === $source ) { 5547 $items = (array) ( self::theme_folder_files_slice( $folder_rel, 0, 0 )['items'] ?? array() ); 5548 $labels = array(); 5549 $preview = array( 'total' => count( $items ), 'done' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0 ); 5550 foreach ( $items as $item ) { 5551 $labels[] = (string) $item['root_slug'] . '/' . (string) $item['rel']; 5552 if ( ! empty( $item['ready'] ) ) { 5553 $preview['done']++; 5554 $preview['bytes_src_total'] += (int) $item['src_bytes']; 5555 $preview['bytes_webp_total'] += (int) $item['dst_bytes']; 5737 if ( ! empty( $reservation['retry'] ) ) { 5738 $labels = (array) ( self::build_theme_folder_preview( $folder_rel )['retry_paths'] ?? array() ); 5739 if ( empty( $labels ) ) { throw new WebpStateException( 'retry_no_files' ); } 5740 $preview = array( 'total' => count( $labels ), 'done' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0 ); 5741 } else { 5742 $items = (array) ( self::theme_folder_files_slice( $folder_rel, 0, 0 )['items'] ?? array() ); 5743 $labels = array(); 5744 $preview = array( 'total' => count( $items ), 'done' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0 ); 5745 foreach ( $items as $item ) { 5746 $labels[] = (string) $item['root_slug'] . '/' . (string) $item['rel']; 5747 if ( ! empty( $item['ready'] ) ) { 5748 $preview['done']++; 5749 $preview['bytes_src_total'] += (int) $item['src_bytes']; 5750 $preview['bytes_webp_total'] += (int) $item['dst_bytes']; 5751 } 5556 5752 } 5557 5753 } … … 5587 5783 5588 5784 $manifest = new WebpManifestMassOrchestrator(); 5589 $sealed = $manifest->build_sealed( $folder_rel ); 5785 $retry_paths = null; 5786 if ( ! empty( $reservation['retry'] ) ) { 5787 UploadsPhysicalScopeSummary::invalidate_request_cache(); 5788 $retry_paths = ( new UploadsPhysicalScopeSummary() )->retry_paths( $folder_rel ); 5789 if ( empty( $retry_paths ) ) { throw new WebpStateException( 'retry_no_files' ); } 5790 } 5791 $sealed = $manifest->build_sealed( $folder_rel, $retry_paths ); 5590 5792 if ( empty( $sealed['ok'] ) ) { 5591 5793 throw new WebpStateException( 'manifest_build_failed' ); … … 5598 5800 throw new WebpStateException( 'manifest_build_invalid' ); 5599 5801 } 5600 try { $statistics = WebpRunStatistics::initialize( $manifest_id ); }5802 try { $statistics = WebpRunStatistics::initialize( $manifest_id, ! empty( $reservation['retry'] ) ); } 5601 5803 catch ( \RuntimeException $error ) { throw new WebpStateException( esc_html( $error->getMessage() ) ); } 5602 5804 … … 5636 5838 catch ( WebpStateException $error ) { 5637 5839 $state = WebpRunState::fail( $reservation, $error->reason(), static function () : void { self::assert_mass_owner(); }, true ); 5638 return array( 'ok' => false, 'code' => $error->reason(), 'state' => $state, 'message' => __( 'Physical uploads manifest could not be sealed. No generation was started.', 'veloimg' ) );5840 return array( 'ok' => false, 'code' => $error->reason(), 'state' => $state, 'message' => 'retry_no_files' === $error->reason() ? __( 'No unsuccessful WebP files eligible for retry remain in this selection. Refresh the statistics.', 'veloimg' ) : __( 'Physical uploads manifest could not be sealed. No generation was started.', 'veloimg' ) ); 5639 5841 } 5640 5842 // A Stop during the builder keeps stopping; publication cannot reopen the activation. … … 5666 5868 if ( empty( $san['ok'] ) ) { throw new WebpStateException( 'mass_folder_invalid' ); } 5667 5869 if ( 'uploads' === $source && ! Installer::exact_identity_cutover_ready() ) { throw new WebpStateException( 'exact_identity_not_ready' ); } 5870 $retry = array(); 5871 if ( '' !== self::request_text( 'retry_quality' ) ) { 5872 $report = Checker::check(); 5873 try { $retry = WebpRetry::options( self::request_text( 'retry_quality' ), self::request_key( 'retry_engine', 'auto' ), (array) ( $report['caps'] ?? array() ) ); } 5874 catch ( \InvalidArgumentException $error ) { throw new WebpStateException( esc_html( $error->getMessage() ) ); } 5875 } 5668 5876 return array( 5669 5877 'source' => $source, 5670 5878 'folder' => (string) $san['folder'], 5671 5879 'mode' => $mode, 5880 'retry' => $retry, 5672 5881 // These values only preserve the already rendered snapshot while the 5673 5882 // new immutable manifest is sealed. They are not worker counters. … … 5800 6009 5801 6010 try { 5802 $result = $resolver->generate_theme_asset_result( (string) ( $item['original_url'] ?? '' ) );6011 $result = $resolver->generate_theme_asset_result( (string) ( $item['original_url'] ?? '' ), (array) ( $state['retry'] ?? array() ) ); 5803 6012 $generation = (array) ( $result['generation_result'] ?? array() ); 5804 6013 $post_status = self::theme_file_status( -
veloimg/trunk/src/Conversion/UploadsPhysicalScopeSummary.php
r3728637 r3739629 39 39 $scope = $this->normalize_scope( $scope ); 40 40 return '__all__' === $scope ? (array) $all['all'] : (array) ( $all['groups'][ $scope ] ?? $this->empty_summary( $scope ) ); 41 } 42 43 /** Selected terminal failures from the same read-only classification as the UI. */ 44 public function retry_paths( string $scope ) : array { 45 $all = $this->all(); 46 return (array) ( $all['retry_paths'][ $this->normalize_scope( $scope ) ] ?? array() ); 41 47 } 42 48 … … 51 57 $outside = $this->empty_summary( '' ); 52 58 $groups = array(); 59 $retry_paths = array( '__all__' => array() ); 53 60 $queue = $this->queue_states(); 54 61 $activity = $this->activity_by_original( $queue['activity'], $this->files_activity() ); … … 73 80 if ( null === $file ) { continue; } 74 81 $group = $this->group_for( $original_rel ); 82 if ( ! empty( $file['retryable'] ) ) { 83 $retry_paths['__all__'][] = $original_rel; 84 if ( '' !== $group ) { $retry_paths[ $group ][] = $original_rel; } 85 } 75 86 if ( '' !== $group && ! isset( $groups[ $group ] ) ) { 76 87 $groups[ $group ] = $this->empty_summary( $group ); … … 92 103 unset( $summary ); 93 104 ksort( $groups, SORT_STRING ); 94 self::$request_cache[ $key ] = array( 'all' => $all, 'groups' => $groups, 'queue_rows' => $queue['rows'], 'source_file_count' => $all['total'] );105 self::$request_cache[ $key ] = array( 'all' => $all, 'groups' => $groups, 'queue_rows' => $queue['rows'], 'source_file_count' => $all['total'], 'retry_paths' => $retry_paths ); 95 106 return self::$request_cache[ $key ]; 96 107 } … … 113 124 if ( $failed ) { $reason = FailureReasons::normalize( FailureReasons::WEBP, (string) ( $errors[ $original_rel ] ?? 'not_saved' ) ); } 114 125 return array( 'type' => 'png' === $ext ? 'png' : 'jpg', 'ready' => $ready, 'failed' => $failed, 126 'retryable' => $failed && WebpRetry::candidate( $original_abs, $reason ), 115 127 'pending' => ! $ready && 'pending' === $status, 'processing' => ! $ready && 'processing' === $status, 116 128 'reason' => $reason, 'all_original_bytes' => $src_bytes, 'src_bytes' => $ready ? $src_bytes : 0, … … 121 133 private function add_file( array &$summary, array $file ) : void { 122 134 $summary['total']++; $summary[ $file['type'] ]++; 123 foreach ( array( 'ready', 'failed', 'pending', 'processing' ) as $field ) { $summary[ $field ] +=(int) $file[ $field ]; }135 foreach ( array( 'ready', 'failed', 'pending', 'processing', 'retryable' ) as $field ) { $summary[ $field ] = (int) ( $summary[ $field ] ?? 0 ) + (int) $file[ $field ]; } 124 136 $summary['bytes_src_total'] += $file['src_bytes']; $summary['bytes_webp_total'] += $file['webp_bytes']; 125 137 $summary['all_original_bytes'] += $file['all_original_bytes']; … … 175 187 /** @return array<string,mixed> */ 176 188 private function empty_summary( string $scope ) : array { 177 return array( 'scope' => $scope, 'total' => 0, 'ready' => 0, 'failed' => 0, ' pending' => 0, 'processing' => 0, 'processed' => 0, 'remaining' => 0, 'outside_months' => 0, 'reason_counts' => array(), 'jpg' => 0, 'png' => 0, 'all_original_bytes' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0, 'last_activity' => '' );189 return array( 'scope' => $scope, 'total' => 0, 'ready' => 0, 'failed' => 0, 'retryable' => 0, 'pending' => 0, 'processing' => 0, 'processed' => 0, 'remaining' => 0, 'outside_months' => 0, 'reason_counts' => array(), 'jpg' => 0, 'png' => 0, 'all_original_bytes' => 0, 'bytes_src_total' => 0, 'bytes_webp_total' => 0, 'last_activity' => '' ); 178 190 } 179 191 -
veloimg/trunk/src/Conversion/WebpManifestMassOrchestrator.php
r3678590 r3739629 29 29 * @return array<string,mixed> 30 30 */ 31 public function build_sealed( string $folder_rel ) : array {31 public function build_sealed( string $folder_rel, ?array $selected_paths = null ) : array { 32 32 $scope = $this->scope( $folder_rel ); 33 33 if ( '' === $scope ) { … … 35 35 } 36 36 37 $paths = $this->inventory->files_for_folder( $scope ); 37 // A retry supplies server-classified failures, never paths from the browser. 38 $paths = null === $selected_paths ? $this->inventory->files_for_folder( $scope ) : $selected_paths; 38 39 $begin = $this->manifests->begin_snapshot( 'uploads', $scope ); 39 40 if ( empty( $begin['ok'] ) ) { -
veloimg/trunk/src/Database/WebpRunStatistics.php
r3715784 r3739629 61 61 62 62 /** Initial reconciliation is once at Start, never in a polling/result loop. */ 63 public static function initialize( string $id ) : array {63 public static function initialize( string $id, bool $retry = false ) : array { 64 64 global $wpdb; 65 65 $manifests = new RunManifestRepository(); … … 84 84 foreach ( $items as $item ) { 85 85 $path = (string) $item['original_path']; 86 if ( ! self::observe( $path, '', '', (array) ( $records[ $path ] ?? array() ), (array) ( $queues[ DerivativesStore::exact_path_key( $path ) ] ?? array() ) ) ) { throw new \RuntimeException( 'statistics_reconciliation_failed' ); } 86 // An explicit retry begins a new attempt for this sealed subset, not 87 // a run already at 100% because its previous failures were terminal. 88 if ( ! self::observe( $path, $retry ? 'pending' : '', '', (array) ( $records[ $path ] ?? array() ), (array) ( $queues[ DerivativesStore::exact_path_key( $path ) ] ?? array() ) ) ) { throw new \RuntimeException( 'statistics_reconciliation_failed' ); } 87 89 $cursor = (int) $item['ordinal']; 88 90 } -
veloimg/trunk/src/Delivery/FormatStrategy.php
r3715784 r3739629 8 8 /** @param array<string,mixed> $settings */ 9 9 public static function order_for( array $settings ) : array { 10 $strategy = sanitize_key( (string) ( $settings['image_strategy'] ?? ' webp_only' ) );10 $strategy = sanitize_key( (string) ( $settings['image_strategy'] ?? 'avif_webp' ) ); 11 11 $legacy = array( 12 12 'max_savings' => 'avif_webp', -
veloimg/trunk/src/Delivery/Resolver.php
r3728637 r3739629 9 9 use VeloWP\Core\FailureReasons; 10 10 use VeloWP\Core\FileSystem; 11 use VeloWP\Conversion\WebpRetry; 11 12 use VeloWP\Concurrency\AtomicLease; 12 13 use VeloWP\Concurrency\LeaseLostException; … … 342 343 * @return array<string,mixed> 343 344 */ 344 public function generate_theme_asset_result( string $url ) : array {345 public function generate_theme_asset_result( string $url, array $retry = array() ) : array { 345 346 if ( ! $this->theme_assets_mode_enabled() ) { 346 347 return array( 'ok' => false ); 347 348 } 348 return $this->resolve_theme_asset( $url, true, true );349 return $this->resolve_theme_asset( $url, true, true, $retry ); 349 350 } 350 351 … … 570 571 * @return array<string,mixed> 571 572 */ 572 private function resolve_theme_asset( string $url, bool $allow_generation = true, bool $include_generation_result = false ) : array {573 private function resolve_theme_asset( string $url, bool $allow_generation = true, bool $include_generation_result = false, array $retry = array() ) : array { 573 574 $url = self::strip_query( $url ); 574 575 $url_path = (string) wp_parse_url( $url, PHP_URL_PATH ); … … 624 625 AtomicLease::assert_owned( (string) $lease['token'] ); 625 626 if ( ! $this->is_theme_derivative_usable( $original_abs, $webp_abs ) ) { 626 $generation_result = $this->ensure_theme_derivative_result( $original_abs, $webp_abs, $webp_rel, $webp_url );627 $generation_result = $this->ensure_theme_derivative_result( $original_abs, $webp_abs, $webp_rel, $webp_url, $retry ); 627 628 AtomicLease::assert_owned( (string) $lease['token'] ); 628 629 $generated = ! empty( $generation_result['ok'] ) && 'done' === (string) ( $generation_result['status'] ?? '' ); … … 674 675 * @return array<string,mixed> 675 676 */ 676 private function ensure_theme_derivative_result( string $original_abs, string $webp_abs, string $webp_rel, string $webp_url ) : array {677 private function ensure_theme_derivative_result( string $original_abs, string $webp_abs, string $webp_rel, string $webp_url, array $retry = array() ) : array { 677 678 $ext = strtolower( (string) pathinfo( $original_abs, PATHINFO_EXTENSION ) ); 678 679 if ( ! in_array( $ext, array( 'jpg', 'jpeg', 'png' ), true ) ) { … … 697 698 698 699 $max_bytes = $src_bytes > 0 ? $src_bytes : 0; 699 $settings = (array) get_option( 'velowp_settings', array());700 $settings = WebpRetry::settings( (array) get_option( 'velowp_settings', array() ), $retry ); 700 701 $quality = $this->quality( $settings ); 701 702 $png_mode = $this->png_mode( $settings ); 702 703 $webp_mode = $this->webp_mode( $settings ); 703 704 $png_quality = $this->png_quality( $settings ); 704 $caps = $this->caps();705 $caps = WebpRetry::caps( $this->caps(), $retry ); 705 706 if ( ( empty( $caps['imagick_webp'] ) || ! class_exists( '\Imagick' ) ) && ( empty( $caps['gd_webp'] ) || ! function_exists( 'imagewebp' ) ) ) { 706 707 return $this->theme_generation_error( 'engine_missing', $src_bytes, 0, $webp_rel ); -
veloimg/trunk/src/Lifecycle/Activate.php
r3728637 r3739629 22 22 // Ensure default settings exist. 23 23 $defaults = array( 24 'image_strategy' => ' webp_only',24 'image_strategy' => 'avif_webp', 25 25 'delivery_method' => 'off', 26 26 'bypass_param' => 'no_veloimg', -
veloimg/trunk/src/Queue/Worker.php
r3715784 r3739629 6 6 7 7 use VeloWP\Converter\Processor; 8 use VeloWP\Conversion\WebpRetry; 8 9 use VeloWP\Concurrency\LeaseLostException; 9 10 use VeloWP\Core\Checker; … … 181 182 $files_repo = new FilesRepository(); 182 183 $proc = new Processor( $store, $mapper, $files_repo, (array) ( $report['caps'] ?? array() ), $settings, array( 'assert_ownership' => static function () use ( $token ) : void { Lock::assert_owned( $token ); } ) ); 184 $retry_proc = null; 183 185 184 186 foreach ( $pending as $task ) { … … 243 245 } 244 246 245 $res = $proc->generate_from_rel( $orig, false ); 247 // Generic ticks may also process automatic uploads. Only rows owned by 248 // this exact retry run receive its immutable settings, including after Resume. 249 $task_proc = $proc; 250 if ( '' !== $task_run_id && $task_run_id === (string) ( $mass_activation['run_id'] ?? '' ) && ! empty( $mass_activation['retry'] ) ) { 251 if ( null === $retry_proc ) { 252 $retry = (array) $mass_activation['retry']; 253 $retry_proc = new Processor( $store, $mapper, $files_repo, WebpRetry::caps( (array) ( $report['caps'] ?? array() ), $retry ), WebpRetry::settings( $settings, $retry ), array( 'assert_ownership' => static function () use ( $token ) : void { Lock::assert_owned( $token ); } ) ); 254 } 255 $task_proc = $retry_proc; 256 } 257 $res = $task_proc->generate_from_rel( $orig, false ); 246 258 Lock::assert_owned( $token ); 247 259 if ( empty( $res['ok'] ) ) { -
veloimg/trunk/templates/admin-avif.php
r3728637 r3739629 12 12 * - $avif_jpeg_quality (int) 13 13 * - $avif_png_quality (int) 14 * - $avif_candidates (array)15 * - $avif_quick (array)16 14 * - $avif_folder_candidates (array) 17 15 * - $theme_asset_candidates (array) … … 25 23 } 26 24 27 $velowp_avif_prefill = '';28 if ( empty( $avif_quick['submitted'] ) && empty( $avif_quick['input'] ) && ! empty( $avif_candidates[0]['url'] ) ) {29 $velowp_avif_prefill = (string) $avif_candidates[0]['url'];30 }31 $velowp_avif_quick_input_prefill = (string) ( $avif_quick['input'] ?? $velowp_avif_prefill );32 $velowp_avif_generate_attrs = array(33 'id' => 'velowp-avif-quick-generate',34 'data-velowp-avif-ready' => $avif_ready ? '1' : '0',35 );36 if ( '' === trim( $velowp_avif_quick_input_prefill ) || ! $avif_ready ) {37 $velowp_avif_generate_attrs['disabled'] = 'disabled';38 }39 40 25 $velowp_avif_mass_state = isset( $avif_mass_status['state'] ) && is_array( $avif_mass_status['state'] ) ? (array) $avif_mass_status['state'] : array(); 41 26 $velowp_avif_mass_folder = (string) ( $velowp_avif_mass_state['folder'] ?? '' ); … … 43 28 $velowp_avif_mass_is_theme = 'theme' === $velowp_avif_mass_source; 44 29 $velowp_avif_panel = 'mass'; 45 if ( ! empty( $avif_quick['submitted'] ) ) { 46 $velowp_avif_panel = 'quick'; 47 } elseif ( empty( $avif_mass_status['active'] ) && isset( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Settings API redirect flag; only selects the visible panel. 30 if ( empty( $avif_mass_status['active'] ) && isset( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Settings API redirect flag; only selects the visible panel. 48 31 $velowp_avif_panel = 'setup'; 49 32 } … … 74 57 </div> 75 58 76 <div class="velowp-topbar__right">77 <button type="button" class="velowp-topbtn" data-panel="quick"><?php echo esc_html__( 'Quick check', 'veloimg' ); ?></button>78 </div>79 59 </div> 80 60 … … 111 91 <output class="velowp-range__out" id="velowp-avif-jpeg-quality-output"><?php echo esc_html( (string) $avif_jpeg_quality ); ?></output> 112 92 </div> 93 <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div> 113 94 <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality AVIF. Lower values reduce file size.', 'veloimg' ); ?></p> 114 95 </td> … … 130 111 <output class="velowp-range__out" id="velowp-avif-png-quality-output"><?php echo esc_html( (string) $avif_png_quality ); ?></output> 131 112 </div> 113 <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div> 132 114 <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality AVIF. Lower values reduce file size.', 'veloimg' ); ?></p> 133 115 <p class="description"><?php echo esc_html__( 'These settings will be used by future AVIF generation. They do not start conversion by themselves.', 'veloimg' ); ?></p> … … 217 199 </section> 218 200 219 <section class="velowp-panel" data-panel="quick">220 <form method="post" class="velowp-card velowp-quick-card" action="#quick">221 <?php wp_nonce_field( 'velowp_avif_quick', 'velowp_avif_quick_nonce' ); ?>222 223 <input type="hidden" name="velowp_avif_quick_input" id="velowp-avif-quick-input" value="<?php echo esc_attr( $velowp_avif_quick_input_prefill ); ?>" />224 225 <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Test AVIF generation on one file', 'veloimg' ); ?></h3>226 <p class="description"><?php echo esc_html__( 'Pick one image and run a safe AVIF test conversion. This helps confirm that AVIF generation works before starting bulk processing.', 'veloimg' ); ?></p>227 228 <div class="velowp-quick-inline velowp-quick-inline--top">229 <?php if ( ! empty( $avif_candidates ) ) : ?>230 <div class="velowp-quick-pick">231 <select id="velowp-avif-candidate-select" class="velowp-select velowp-select--wide">232 <option value=""><?php echo esc_html__( 'Select', 'veloimg' ); ?></option>233 <?php foreach ( (array) $avif_candidates as $velowp_candidate ) : ?>234 <?php235 $velowp_candidate_url = (string) ( $velowp_candidate['url'] ?? '' );236 $velowp_candidate_size = (int) ( $velowp_candidate['size'] ?? 0 );237 $velowp_width = (int) ( $velowp_candidate['width'] ?? 0 );238 $velowp_height = (int) ( $velowp_candidate['height'] ?? 0 );239 $velowp_candidate_label = '';240 if ( ! empty( $velowp_candidate['title'] ) ) {241 $velowp_candidate_label .= (string) $velowp_candidate['title'] . ' - ';242 }243 if ( $velowp_width > 0 && $velowp_height > 0 ) {244 $velowp_candidate_label .= $velowp_width . 'x' . $velowp_height;245 }246 if ( $velowp_candidate_size > 0 ) {247 $velowp_candidate_label .= ' - ' . size_format( $velowp_candidate_size, 2 );248 }249 if ( '' === $velowp_candidate_label ) {250 $velowp_candidate_label = $velowp_candidate_url;251 }252 ?>253 <option value="<?php echo esc_attr( $velowp_candidate_url ); ?>" <?php selected( $velowp_avif_quick_input_prefill, $velowp_candidate_url ); ?>><?php echo esc_html( $velowp_candidate_label ); ?></option>254 <?php endforeach; ?>255 </select>256 </div>257 <?php else : ?>258 <div class="velowp-quick-pick velowp-quick-pick--fallback">259 <input260 type="text"261 name="velowp_avif_quick_input"262 id="velowp-avif-quick-input-fallback"263 class="regular-text velowp-quick-input"264 value="<?php echo esc_attr( $velowp_avif_quick_input_prefill ); ?>"265 placeholder="https://example.com/wp-content/uploads/2025/01/photo.jpg"266 />267 </div>268 <?php endif; ?>269 270 <div class="velowp-actions velowp-actions--launch velowp-actions--inline">271 <button type="submit" class="button button-primary velowp-btn-glass velowp-btn-glass--primary"272 <?php foreach ( $velowp_avif_generate_attrs as $velowp_attr_name => $velowp_attr_value ) : ?>273 <?php if ( 'disabled' === (string) $velowp_attr_name ) : ?>274 disabled275 <?php else : ?>276 <?php echo esc_attr( (string) $velowp_attr_name ); ?>="<?php echo esc_attr( (string) $velowp_attr_value ); ?>"277 <?php endif; ?>278 <?php endforeach; ?>279 name="velowp_avif_quick_action" value="convert_now">280 <?php echo esc_html__( 'Generate AVIF', 'veloimg' ); ?>281 </button>282 </div>283 </div>284 285 <?php if ( ! $avif_ready ) : ?>286 <p class="description"><?php echo esc_html__( 'AVIF generation is disabled until the server has an AVIF encoder and a writable derivatives store.', 'veloimg' ); ?></p>287 <?php endif; ?>288 289 <?php if ( ! empty( $avif_quick['submitted'] ) ) : ?>290 <hr class="velowp-hr velowp-hr--quick" />291 <h3 class="velowp-h3 velowp-h3--quick-result"><?php echo esc_html__( 'Result', 'veloimg' ); ?></h3>292 293 <?php if ( ! empty( $avif_quick['verdict']['done'] ) && empty( $avif_quick['verdict']['passed'] ) ) : ?>294 <div class="notice notice-warning inline velowp-quick-notice">295 <p><strong><?php echo esc_html__( 'AVIF test did not create a usable file.', 'veloimg' ); ?></strong></p>296 <?php if ( ! empty( $avif_quick['verdict']['note'] ) ) : ?>297 <p class="description velowp-result-note"><?php echo esc_html( (string) $avif_quick['verdict']['note'] ); ?></p>298 <?php endif; ?>299 </div>300 <?php elseif ( ! empty( $avif_quick['verdict']['passed'] ) ) : ?>301 <div class="notice notice-success inline velowp-quick-notice">302 <p><?php echo esc_html( (string) ( $avif_quick['verdict']['summary'] ?? __( 'AVIF was created and passed validation.', 'veloimg' ) ) ); ?></p>303 </div>304 <?php endif; ?>305 306 <?php if ( ! empty( $avif_quick['original_rel'] ) ) : ?>307 <?php308 $velowp_avif_url = (string) ( $avif_quick['d_expected']['derivative_url'] ?? '' );309 $velowp_avif_exists = ! empty( $avif_quick['d_expected']['exists'] ) && '' !== $velowp_avif_url;310 $velowp_original_bytes = (int) ( $avif_quick['result']['original_bytes'] ?? 0 );311 $velowp_avif_bytes = (int) ( $avif_quick['result']['derivative_bytes'] ?? 0 );312 $velowp_avif_saved_bytes = 0;313 $velowp_avif_saved_percent = 0;314 $velowp_avif_is_valid = ! empty( $avif_quick['verdict']['passed'] ) && $velowp_avif_exists && $velowp_avif_bytes > 0;315 if ( $velowp_avif_is_valid && $velowp_original_bytes > 0 && $velowp_avif_bytes < $velowp_original_bytes ) {316 $velowp_avif_saved_bytes = (int) ( $velowp_original_bytes - $velowp_avif_bytes );317 $velowp_avif_saved_percent = (int) round( ( $velowp_avif_saved_bytes / $velowp_original_bytes ) * 100 );318 }319 $velowp_avif_status = $velowp_avif_is_valid ? __( 'Exists and valid', 'veloimg' ) : ( $velowp_avif_exists ? __( 'Exists, but is not usable', 'veloimg' ) : __( 'Not created', 'veloimg' ) );320 ?>321 <div class="velowp-quick-result-grid">322 <div class="velowp-quick-result-row">323 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original (relative path)', 'veloimg' ); ?></div>324 <div class="velowp-quick-result-value"><code><?php echo esc_html( (string) $avif_quick['original_rel'] ); ?></code></div>325 </div>326 <div class="velowp-quick-result-row">327 <div class="velowp-quick-result-label"><?php echo esc_html( $velowp_avif_exists ? __( 'AVIF URL', 'veloimg' ) : __( 'Expected AVIF path', 'veloimg' ) ); ?></div>328 <div class="velowp-quick-result-value">329 <?php if ( '' !== $velowp_avif_url ) : ?>330 <div class="velowp-quick-result-url">331 <code><?php echo esc_html( $velowp_avif_url ); ?></code>332 <?php if ( $velowp_avif_exists ) : ?>333 <span class="velowp-link-actions"><a class="button button-small velowp-btn-mini-open" href="<?php echo esc_url( $velowp_avif_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html__( 'Open AVIF', 'veloimg' ); ?></a></span>334 <?php endif; ?>335 </div>336 <?php if ( ! $velowp_avif_exists ) : ?>337 <p class="description velowp-result-note"><?php echo esc_html__( 'The file does not exist yet, so it cannot be opened.', 'veloimg' ); ?></p>338 <?php endif; ?>339 <?php else : ?>340 <code>-</code>341 <?php endif; ?>342 </div>343 </div>344 <div class="velowp-quick-result-row">345 <div class="velowp-quick-result-label"><?php echo esc_html__( 'File status', 'veloimg' ); ?></div>346 <div class="velowp-quick-result-value"><?php echo esc_html( $velowp_avif_status ); ?></div>347 </div>348 <?php if ( $velowp_original_bytes > 0 ) : ?>349 <div class="velowp-quick-result-row">350 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original size', 'veloimg' ); ?></div>351 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_original_bytes, 2 ) ); ?></div>352 </div>353 <?php endif; ?>354 <?php if ( $velowp_avif_bytes > 0 ) : ?>355 <div class="velowp-quick-result-row">356 <div class="velowp-quick-result-label"><?php echo esc_html__( 'AVIF size', 'veloimg' ); ?></div>357 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_avif_bytes, 2 ) ); ?></div>358 </div>359 <?php endif; ?>360 <?php if ( $velowp_avif_saved_bytes > 0 ) : ?>361 <div class="velowp-quick-result-row">362 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Savings', 'veloimg' ); ?></div>363 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_avif_saved_bytes, 2 ) . ' (' . $velowp_avif_saved_percent . '%)' ); ?></div>364 </div>365 <?php endif; ?>366 <?php if ( ! empty( $avif_quick['verdict']['reasons'] ) && is_array( $avif_quick['verdict']['reasons'] ) ) : ?>367 <div class="velowp-quick-result-row">368 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Reason / details', 'veloimg' ); ?></div>369 <div class="velowp-quick-result-value">370 <ul class="velowp-list">371 <?php foreach ( (array) $avif_quick['verdict']['reasons'] as $velowp_reason ) : ?>372 <li><?php echo esc_html( (string) $velowp_reason ); ?></li>373 <?php endforeach; ?>374 </ul>375 </div>376 </div>377 <?php endif; ?>378 </div>379 <?php endif; ?>380 <?php endif; ?>381 </form>382 </section>383 201 384 202 <section class="velowp-panel" data-panel="mass"> … … 389 207 <div> 390 208 <h2 class="velowp-h2 velowp-h2--tight"><?php echo esc_html__( 'AVIF generation', 'veloimg' ); ?></h2> 391 <p class="description"><?php echo esc_html__( 'Choose the entire Media Library, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only AVIF derivatives are created.', 'veloimg' ); ?></p>209 <p class="description"><?php echo esc_html__( 'Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only AVIF derivatives are created.', 'veloimg' ); ?></p> 392 210 </div> 393 211 </div> … … 396 214 <label class="velowp-seg"> 397 215 <input type="radio" name="velowp_avif_mass_mode" id="velowp-avif-mass-mode-all" value="all" <?php checked( ! $velowp_avif_mass_is_theme && ( '' === $velowp_avif_mass_folder || '__all__' === $velowp_avif_mass_folder ) ); ?> /> 398 <span><?php echo esc_html__( ' Entire Media Library', 'veloimg' ); ?></span>216 <span><?php echo esc_html__( 'All uploads', 'veloimg' ); ?></span> 399 217 </label> 400 218 <label class="velowp-seg"> … … 484 302 </div> 485 303 <p class="description" id="velowp-avif-mass-progress-text"><?php echo esc_html__( 'No AVIF generation run yet.', 'veloimg' ); ?></p> 304 <?php $velowp_generation_format = 'avif'; require VELOWP_DIR . 'templates/generation-concurrency.php'; ?> 486 305 487 306 <div class="velowp-mass-actions velowp-mass-actions--row"> … … 529 348 <p id="velowp-avif-outside-months-summary" class="velowp-muted velowp-hidden"></p> 530 349 350 <div class="velowp-table-scroll" tabindex="0" role="region" aria-label="<?php echo esc_attr__( 'Uploads folder statistics', 'veloimg' ); ?>"> 531 351 <table class="widefat striped velowp-table-tight velowp-folders-table" id="velowp-avif-folders-overview"> 532 352 <thead> … … 545 365 </tbody> 546 366 </table> 367 </div> 547 368 <p class="velowp-folder-more-wrap"><button type="button" class="button" id="velowp-avif-folders-more" hidden><?php echo esc_html__( 'Show more', 'veloimg' ); ?></button></p> 548 369 </div> -
veloimg/trunk/templates/admin-dashboard.php
r3728637 r3739629 9 9 * - $lock_status (array) 10 10 * - $next_tick (int) 11 * - $quick (array)12 * - $candidates (array)13 11 * - $scan (array) 14 12 * - $active_panel (string) … … 25 23 26 24 $velowp_panel = is_string( $active_panel ?? null ) && '' !== (string) $active_panel ? (string) $active_panel : 'mass'; 27 28 // Prefill: pick the largest recent image.29 $velowp_prefill = '';30 if ( empty( $quick['submitted'] ) && empty( $quick['input'] ) && ! empty( $candidates[0]['url'] ) ) {31 $velowp_prefill = (string) $candidates[0]['url'];32 }33 $velowp_quick_input_prefill = (string) ( $quick['input'] ?? $velowp_prefill );34 25 35 26 $velowp_scan_state = isset( $scan['state'] ) && is_array( $scan['state'] ) ? (array) $scan['state'] : array(); … … 71 62 </div> 72 63 73 <div class="velowp-topbar__right">74 <button type="button" class="velowp-topbtn" data-velowp-switch="quick"><?php echo esc_html__( 'Quick check', 'veloimg' ); ?></button>75 </div>76 64 </div> 77 65 … … 87 75 <div class="velowp-card"> 88 76 <h2 class="velowp-h2"><?php echo esc_html__( 'Generation settings', 'veloimg' ); ?></h2> 89 <p class="description"><?php echo esc_html__( 'Configure the parameters first, then run a quick generation check. Only generation is checked here; frontend delivery may still be disabled.', 'veloimg' ); ?></p>77 <p class="description"><?php echo esc_html__( 'Configure generation here. Single-file tests are in Diagnostics on the VeloIMG home page.', 'veloimg' ); ?></p> 90 78 91 79 <form method="post" action="options.php" class="velowp-settings-form"> … … 113 101 <output class="velowp-range__out" id="velowp-webp-jpeg-quality-output"><?php echo esc_html( (string) $velowp_webp_jpeg_quality ); ?></output> 114 102 </div> 103 <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div> 115 104 <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality WebP. Lower values reduce file size.', 'veloimg' ); ?></p> 116 105 </td> … … 137 126 <output class="velowp-range__out" id="velowp-webp-png-quality-output"><?php echo esc_html( (string) $velowp_webp_png_quality ); ?></output> 138 127 </div> 128 <div class="velowp-range-scale" aria-hidden="true"><span>50</span><span>100</span></div> 139 129 <p class="description"><?php echo esc_html__( 'Default is 85. Set 100 for lossless or maximum-quality WebP. Lower values reduce file size.', 'veloimg' ); ?></p> 140 130 <p class="description"><?php echo esc_html__( 'These settings will be used by future WebP generation. They do not start conversion by themselves.', 'veloimg' ); ?></p> … … 212 202 </section> 213 203 214 <!-- QUICK -->215 <section class="velowp-panel" data-panel="quick">216 <form method="post" class="velowp-card velowp-quick-card" action="#quick">217 <?php wp_nonce_field( 'velowp_quick', 'velowp_quick_nonce' ); ?>218 219 <input type="hidden" name="velowp_quick_input" id="velowp-quick-input" value="<?php echo esc_attr( $velowp_quick_input_prefill ); ?>" />220 221 <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Test generation on one file', 'veloimg' ); ?></h3>222 <p class="description"><?php echo esc_html__( 'Pick one image and run a safe test conversion. This helps confirm that WebP generation works before starting bulk processing.', 'veloimg' ); ?></p>223 224 <div class="velowp-quick-inline velowp-quick-inline--top">225 <?php if ( ! empty( $candidates ) ) : ?>226 <div class="velowp-quick-pick">227 <select id="velowp-candidate-select" class="velowp-select velowp-select--wide">228 <option value=""><?php echo esc_html__( 'Select', 'veloimg' ); ?></option>229 <?php foreach ( (array) $candidates as $velowp_candidate ) : ?>230 <?php231 $velowp_candidate_url = (string) ( $velowp_candidate['url'] ?? '' );232 $velowp_candidate_size = (int) ( $velowp_candidate['size'] ?? 0 );233 $velowp_width = (int) ( $velowp_candidate['width'] ?? 0 );234 $velowp_height = (int) ( $velowp_candidate['height'] ?? 0 );235 $velowp_candidate_label = '';236 if ( ! empty( $velowp_candidate['title'] ) ) {237 $velowp_candidate_label .= (string) $velowp_candidate['title'] . ' - ';238 }239 if ( $velowp_width > 0 && $velowp_height > 0 ) {240 $velowp_candidate_label .= $velowp_width . 'x' . $velowp_height;241 }242 if ( $velowp_candidate_size > 0 ) {243 $velowp_candidate_label .= ' - ' . sprintf( '%.2f MB', $velowp_candidate_size / 1024 / 1024 );244 }245 ?>246 <option value="<?php echo esc_attr( $velowp_candidate_url ); ?>" <?php selected( $velowp_quick_input_prefill, $velowp_candidate_url ); ?>><?php echo esc_html( $velowp_candidate_label ); ?></option>247 <?php endforeach; ?>248 </select>249 </div>250 <?php else : ?>251 <div class="velowp-quick-pick velowp-quick-pick--fallback">252 <input253 type="text"254 name="velowp_quick_input"255 id="velowp-quick-input-fallback"256 class="regular-text velowp-quick-input"257 value="<?php echo esc_attr( $velowp_quick_input_prefill ); ?>"258 placeholder="https://example.com/wp-content/uploads/2025/01/photo.jpg"259 />260 </div>261 <?php endif; ?>262 263 <div class="velowp-actions velowp-actions--launch velowp-actions--inline">264 <button type="submit" class="button button-primary velowp-btn-glass velowp-btn-glass--primary" id="velowp-quick-generate" name="velowp_quick_action" value="convert_now" <?php disabled( '' === trim( $velowp_quick_input_prefill ) ); ?>><?php echo esc_html__( 'Generate WebP', 'veloimg' ); ?></button>265 </div>266 </div>267 268 <?php if ( ! empty( $quick['submitted'] ) ) : ?>269 <hr class="velowp-hr velowp-hr--quick" />270 <h3 class="velowp-h3 velowp-h3--quick-result"><?php echo esc_html__( 'Result', 'veloimg' ); ?></h3>271 272 <?php273 $velowp_quick_verdict = isset( $quick['verdict'] ) && is_array( $quick['verdict'] ) ? (array) $quick['verdict'] : array();274 $velowp_result_kind = (string) ( $velowp_quick_verdict['result_kind'] ?? '' );275 $velowp_notice_class = 'notice-error';276 $velowp_notice_title = __( 'WebP check failed.', 'veloimg' );277 if ( in_array( $velowp_result_kind, array( 'created', 'already_ready' ), true ) ) {278 $velowp_notice_class = 'notice-success';279 $velowp_notice_title = (string) ( $velowp_quick_verdict['summary'] ?? __( 'WebP was created and passed validation.', 'veloimg' ) );280 } elseif ( 'skipped' === $velowp_result_kind ) {281 $velowp_notice_class = 'notice-warning';282 $velowp_notice_title = __( 'WebP was not saved.', 'veloimg' );283 }284 ?>285 <?php if ( ! empty( $velowp_quick_verdict['done'] ) ) : ?>286 <div class="notice <?php echo esc_attr( $velowp_notice_class ); ?> inline velowp-quick-notice">287 <p><strong><?php echo esc_html( $velowp_notice_title ); ?></strong></p>288 289 <?php if ( 'skipped' === $velowp_result_kind ) : ?>290 <p class="description velowp-result-note"><?php echo esc_html__( 'Try another file for testing. Some images are intentionally not saved as WebP when the result would be larger than the original.', 'veloimg' ); ?></p>291 <?php endif; ?>292 </div>293 <?php endif; ?>294 295 <?php if ( ! empty( $quick['original_rel'] ) ) : ?>296 <?php297 $velowp_derivative_url = (string) ( $quick['d_expected']['derivative_url'] ?? '' );298 $velowp_valid_webp_exists = ! empty( $quick['verdict']['derivative_bytes'] ) && in_array( $velowp_result_kind, array( 'created', 'already_ready' ), true );299 $velowp_derivative_url_exists = $velowp_valid_webp_exists && '' !== $velowp_derivative_url;300 $velowp_original_bytes = (int) ( $quick['verdict']['original_bytes'] ?? 0 );301 $velowp_webp_bytes = (int) ( $quick['verdict']['derivative_bytes'] ?? 0 );302 $velowp_saved_bytes = (int) ( $quick['verdict']['saved_bytes'] ?? 0 );303 $velowp_saved_percent = (int) ( $quick['verdict']['saved_percent'] ?? 0 );304 $velowp_file_status = $velowp_valid_webp_exists ? __( 'Exists and valid', 'veloimg' ) : ( ! empty( $quick['d_expected']['exists'] ) ? __( 'Exists, but is not usable', 'veloimg' ) : __( 'Not created', 'veloimg' ) );305 ?>306 <div class="velowp-quick-result-grid">307 <div class="velowp-quick-result-row">308 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original (relative path)', 'veloimg' ); ?></div>309 <div class="velowp-quick-result-value"><code><?php echo esc_html( (string) $quick['original_rel'] ); ?></code></div>310 </div>311 <div class="velowp-quick-result-row">312 <div class="velowp-quick-result-label"><?php echo esc_html( $velowp_derivative_url_exists ? __( 'WebP URL', 'veloimg' ) : __( 'Expected WebP path', 'veloimg' ) ); ?></div>313 <div class="velowp-quick-result-value">314 <?php if ( '' !== $velowp_derivative_url ) : ?>315 <div class="velowp-quick-result-url">316 <code><?php echo esc_html( $velowp_derivative_url ); ?></code>317 <?php if ( $velowp_derivative_url_exists ) : ?>318 <span class="velowp-link-actions"><a class="button button-small velowp-btn-mini-open" href="<?php echo esc_url( $velowp_derivative_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html__( 'Open WebP', 'veloimg' ); ?></a></span>319 <?php endif; ?>320 </div>321 <?php if ( ! $velowp_derivative_url_exists ) : ?>322 <p class="description velowp-result-note"><?php echo esc_html__( 'The file does not exist yet, so it cannot be opened.', 'veloimg' ); ?></p>323 <?php endif; ?>324 <?php else : ?>325 <code>-</code>326 <?php endif; ?>327 </div>328 </div>329 <div class="velowp-quick-result-row">330 <div class="velowp-quick-result-label"><?php echo esc_html__( 'File status', 'veloimg' ); ?></div>331 <div class="velowp-quick-result-value"><?php echo esc_html( $velowp_file_status ); ?></div>332 </div>333 <?php if ( $velowp_original_bytes > 0 ) : ?>334 <div class="velowp-quick-result-row">335 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Original size', 'veloimg' ); ?></div>336 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_original_bytes, 2 ) ); ?></div>337 </div>338 <?php endif; ?>339 <?php if ( $velowp_webp_bytes > 0 ) : ?>340 <div class="velowp-quick-result-row">341 <div class="velowp-quick-result-label"><?php echo esc_html__( 'WebP size', 'veloimg' ); ?></div>342 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_webp_bytes, 2 ) ); ?></div>343 </div>344 <?php endif; ?>345 <?php if ( $velowp_saved_bytes > 0 ) : ?>346 <div class="velowp-quick-result-row">347 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Savings', 'veloimg' ); ?></div>348 <div class="velowp-quick-result-value"><?php echo esc_html( size_format( $velowp_saved_bytes, 2 ) . ' (' . $velowp_saved_percent . '%)' ); ?></div>349 </div>350 <?php endif; ?>351 <?php if ( ! empty( $quick['verdict']['reasons'] ) && is_array( $quick['verdict']['reasons'] ) ) : ?>352 <div class="velowp-quick-result-row">353 <div class="velowp-quick-result-label"><?php echo esc_html__( 'Reason / details', 'veloimg' ); ?></div>354 <div class="velowp-quick-result-value">355 <ul class="velowp-list">356 <?php foreach ( (array) $quick['verdict']['reasons'] as $velowp_reason ) : ?>357 <li><?php echo esc_html( (string) $velowp_reason ); ?></li>358 <?php endforeach; ?>359 </ul>360 </div>361 </div>362 <?php endif; ?>363 </div>364 <?php endif; ?>365 <?php endif; ?>366 </form>367 368 </section>369 204 370 205 … … 374 209 <div class="velowp-mass-grid velowp-mass-grid--top"> 375 210 <!-- MAIN --> 211 <div class="velowp-mass-main-column"> 376 212 <div class="velowp-card velowp-card--glass velowp-mass-card velowp-mass-card--main"> 377 213 <div class="velowp-mass-card__head"> 378 214 <div> 379 215 <h2 class="velowp-h2 velowp-h2--tight"><?php echo esc_html__( 'Bulk WebP generation', 'veloimg' ); ?></h2> 380 <p class="description"><?php echo esc_html__( 'Choose the entire Media Library, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only WebP derivatives are created.', 'veloimg' ); ?></p>216 <p class="description"><?php echo esc_html__( 'Choose all uploads, a month folder inside uploads (YYYY/MM), or a folder from the active theme. Original files are never modified; only WebP derivatives are created.', 'veloimg' ); ?></p> 381 217 </div> 382 218 </div> … … 385 221 <label class="velowp-seg"> 386 222 <input type="radio" name="velowp_mass_mode" id="velowp-mass-mode-all" value="all" checked /> 387 <span><?php echo esc_html__( ' Entire Media Library', 'veloimg' ); ?></span>223 <span><?php echo esc_html__( 'All uploads', 'veloimg' ); ?></span> 388 224 </label> 389 225 <label class="velowp-seg"> … … 462 298 </div> 463 299 <p class="description" id="velowp-mass-status" aria-live="polite"><?php echo esc_html__( 'Choose a folder to see an estimate and the file list.', 'veloimg' ); ?></p> 300 <?php $velowp_generation_format = 'webp'; require VELOWP_DIR . 'templates/generation-concurrency.php'; ?> 464 301 465 302 <div class="velowp-mass-actions velowp-mass-actions--row"> … … 478 315 </div> 479 316 317 <?php require VELOWP_DIR . 'templates/webp-retry.php'; ?> 318 </div> 480 319 <!-- SUMMARY --> 481 320 <div class="velowp-card velowp-card--glass velowp-mass-card velowp-mass-card--summary"> … … 523 362 524 363 <p id="velowp-statistics-status" class="velowp-muted velowp-hidden" role="status" aria-live="polite"></p> 364 <div class="velowp-table-scroll" tabindex="0" role="region" aria-label="<?php echo esc_attr__( 'Uploads folder statistics', 'veloimg' ); ?>"> 525 365 <table class="widefat striped velowp-table-tight velowp-folders-table" id="velowp-folders-overview"> 526 366 <thead> … … 539 379 </tbody> 540 380 </table> 381 </div> 541 382 <p id="velowp-outside-months-summary" class="velowp-muted velowp-hidden" role="status" aria-live="polite"></p> 542 383 <p class="velowp-folder-more-wrap"><button type="button" class="button velowp-hidden" id="velowp-folders-more" hidden disabled><?php echo esc_html__( 'Show more', 'veloimg' ); ?></button></p> -
veloimg/trunk/templates/admin-home.php
r3728637 r3739629 25 25 } 26 26 $velowp_image_strategy_current = isset( $image_strategies[ $image_strategy ] ) && is_array( $image_strategies[ $image_strategy ] ) ? (array) $image_strategies[ $image_strategy ] : array(); 27 $velowp_image_strategy_label = (string) ( $velowp_image_strategy_current['label'] ?? __( 'Unknown strategy', 'veloimg' ) );28 27 $velowp_image_strategy_desc = (string) ( $velowp_image_strategy_current['description'] ?? '' ); 29 28 $velowp_delivery_enabled = 'off' !== $delivery_method; … … 102 101 <div data-velowp-home-result="avif"><?php echo \VeloWP\Admin\OptimizationSummary::home_html( 'avif' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shared template escapes each value. ?></div> 103 102 </div> 104 <div class="velowp-home-status-card is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>"> 105 <div class="velowp-home-status-card__head"> 106 <strong><?php echo esc_html__( 'Frontend delivery', 'veloimg' ); ?></strong> 107 <span class="velowp-self-test-badge is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>"><?php echo esc_html( $delivery_status ); ?></span> 103 <div class="velowp-home-status-card is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" data-velowp-delivery-card> 104 <div class="velowp-home-status-card__head"> 105 <strong id="velowp-delivery-label"><?php echo esc_html__( 'Frontend delivery', 'veloimg' ); ?></strong> 106 <select class="velowp-self-test-badge velowp-home-badge-select is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" name="velowp_settings[delivery_method]" id="velowp-delivery-method" form="velowp-home-settings-form" aria-labelledby="velowp-delivery-label" aria-describedby="velowp-delivery-description"> 107 <?php foreach ( $delivery_labels as $velowp_delivery_key => $velowp_delivery_label ) : ?> 108 <option value="<?php echo esc_attr( $velowp_delivery_key ); ?>" <?php selected( $delivery_method, $velowp_delivery_key ); ?>><?php echo 'off' === $velowp_delivery_key ? esc_html__( 'Off', 'veloimg' ) : esc_html( (string) $velowp_delivery_label ); ?></option> 109 <?php endforeach; ?> 110 </select> 108 111 </div> 109 112 <p><?php echo esc_html__( 'Controls whether generated files are served to visitors.', 'veloimg' ); ?></p> 110 <p class="description"> 111 <?php 112 if ( 'php_safe' === $delivery_method ) { 113 echo esc_html__( 'PHP Safe adds picture/source markup in WordPress. If the layout breaks, you may need to add CSS for picture elements.', 'veloimg' ); 114 } elseif ( 'apache' === $delivery_method ) { 115 echo esc_html__( 'Apache / LiteSpeed keeps HTML unchanged and serves generated AVIF/WebP files through uploads/.htaccess when the browser accepts them.', 'veloimg' ); 116 } else { 117 echo esc_html__( 'Visitors receive original images until frontend delivery is enabled.', 'veloimg' ); 118 } 119 ?> 120 </p> 121 </div> 122 <div class="velowp-home-status-card velowp-home-status-card--strategy is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>"> 123 <div class="velowp-home-status-card__head"> 124 <strong><?php echo esc_html__( 'Frontend format priority', 'veloimg' ); ?></strong> 125 </div> 126 <div class="velowp-home-strategy-value is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>"><?php echo esc_html( $velowp_image_strategy_label ); ?></div> 127 <p><?php echo '' !== $velowp_image_strategy_desc ? esc_html( $velowp_image_strategy_desc ) : esc_html__( 'No frontend strategy selected.', 'veloimg' ); ?></p> 113 <div id="velowp-delivery-description"> 114 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="off" <?php echo 'off' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Visitors receive original images until frontend delivery is enabled.', 'veloimg' ); ?></p> 115 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="php_safe" <?php echo 'php_safe' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'PHP Safe adds picture/source markup in WordPress. If the layout breaks, you may need to add CSS for picture elements.', 'veloimg' ); ?></p> 116 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="apache" <?php echo 'apache' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Apache / LiteSpeed keeps HTML unchanged and serves generated AVIF/WebP files through uploads/.htaccess when the browser accepts them.', 'veloimg' ); ?></p> 117 </div> 118 </div> 119 <div class="velowp-home-status-card velowp-home-status-card--strategy is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" data-velowp-delivery-card> 120 <div class="velowp-home-status-card__head"> 121 <strong id="velowp-strategy-label"><?php echo esc_html__( 'Frontend format priority', 'veloimg' ); ?></strong> 122 </div> 123 <select class="velowp-home-strategy-value velowp-home-badge-select is-<?php echo esc_attr( $velowp_delivery_card_status ); ?>" name="velowp_settings[image_strategy]" id="velowp-image-strategy" form="velowp-home-settings-form" aria-labelledby="velowp-strategy-label" aria-describedby="velowp-strategy-description"> 124 <?php foreach ( $image_strategies as $velowp_strategy_key => $velowp_strategy ) : ?> 125 <option value="<?php echo esc_attr( $velowp_strategy_key ); ?>" data-description="<?php echo esc_attr( (string) ( $velowp_strategy['description'] ?? '' ) ); ?>" <?php selected( $image_strategy, $velowp_strategy_key ); ?>><?php echo esc_html( (string) ( $velowp_strategy['label'] ?? '' ) ); ?></option> 126 <?php endforeach; ?> 127 </select> 128 <p id="velowp-strategy-description"><?php echo '' !== $velowp_image_strategy_desc ? esc_html( $velowp_image_strategy_desc ) : esc_html__( 'No frontend strategy selected.', 'veloimg' ); ?></p> 128 129 <p class="description"><?php echo esc_html__( 'Format priority is applied only when frontend delivery is enabled.', 'veloimg' ); ?></p> 129 130 </div> 130 131 </div> 131 132 132 <div class="velowp-card velowp-quick-card velowp-self-test-card" id="velowp-self-test"> 133 <form method="post" action="options.php" class="velowp-settings-form" id="velowp-home-settings-form"> 134 <?php settings_fields( 'velowp_settings_group' ); ?> 135 <input type="hidden" name="velowp_settings[_form]" value="home" /> 136 137 <?php submit_button( __( 'Save settings', 'veloimg' ) ); ?> 138 </form> 139 140 <details class="velowp-card velowp-quick-card velowp-home-accordion velowp-self-test-card" id="velowp-self-test" <?php echo $auto_self_test ? 'open' : ''; ?>> 141 <summary><h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check setup', 'veloimg' ); ?></h3></summary> 142 <div class="velowp-home-accordion__body"> 133 143 <div class="velowp-self-test-head"> 134 144 <div> 135 <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check setup', 'veloimg' ); ?></h3>136 145 <p class="description"><?php echo esc_html__( 'Run a diagnostic check for generation, storage and the currently selected frontend delivery mode. Original images are not touched and bulk conversion is not started; AVIF checks may create temporary files and delete them immediately.', 'veloimg' ); ?></p> 137 146 </div> … … 178 187 <?php endif; ?> 179 188 </div> 180 </div> 181 182 <div class="velowp-card velowp-quick-card velowp-url-audit-card" id="velowp-url-audit"> 183 <div class="velowp-self-test-head"> 184 <div> 185 <h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check frontend URL', 'veloimg' ); ?></h3> 186 <p class="description"><?php echo esc_html__( 'Scan one page from this site and show what visitors actually receive: AVIF, WebP, original fallback, source order or Apache response rewrites.', 'veloimg' ); ?></p> 187 </div> 188 </div> 189 <div class="velowp-url-audit-form"> 190 <input type="url" id="velowp-url-audit-input" class="regular-text" value="<?php echo esc_attr( home_url( '/' ) ); ?>" placeholder="<?php echo esc_attr( home_url( '/' ) ); ?>" /> 191 <button type="button" class="button button-primary velowp-btn-glass velowp-btn-glass--primary" id="velowp-url-audit-run" <?php disabled( 'off' === $delivery_method ); ?>><?php echo esc_html__( 'Check page', 'veloimg' ); ?></button> 192 </div> 193 <p class="description"><?php echo esc_html__( 'Only URLs from the current site are allowed. The check does not start generation and does not change files.', 'veloimg' ); ?></p> 194 <p class="description"><?php echo esc_html__( 'If a page cache is active, clear it after changing frontend delivery; cached HTML can keep old markup until the cache expires.', 'veloimg' ); ?></p> 195 <div class="velowp-url-audit-result" id="velowp-url-audit-result" aria-live="polite"> 196 <p class="description velowp-muted"><?php echo 'off' === $delivery_method ? esc_html__( 'Frontend delivery is disabled. Enable PHP Safe or Apache /.htaccess before running a page delivery check.', 'veloimg' ) : esc_html__( 'No page check has been run yet.', 'veloimg' ); ?></p> 197 </div> 198 </div> 199 200 <form method="post" action="options.php" class="velowp-settings-form"> 201 <?php settings_fields( 'velowp_settings_group' ); ?> 202 <input type="hidden" name="velowp_settings[_form]" value="home" /> 203 204 <h3><?php echo esc_html__( 'Frontend format priority', 'veloimg' ); ?></h3> 205 <fieldset class="velowp-strategy-options"> 206 <?php foreach ( $image_strategies as $velowp_strategy_key => $velowp_strategy ) : ?> 207 <label class="velowp-strategy-option"> 208 <input type="radio" name="velowp_settings[image_strategy]" value="<?php echo esc_attr( $velowp_strategy_key ); ?>" <?php checked( $image_strategy, $velowp_strategy_key ); ?> /> 209 <strong><?php echo esc_html( (string) ( $velowp_strategy['label'] ?? '' ) ); ?></strong> 210 <br /> 211 <span class="description"><?php echo esc_html( (string) ( $velowp_strategy['description'] ?? '' ) ); ?></span> 212 </label> 213 <?php endforeach; ?> 214 </fieldset> 215 189 </div> 190 </details> 191 192 <details class="velowp-card velowp-quick-card velowp-home-accordion" id="velowp-home-diagnostics" <?php echo $diagnostics_open ? 'open' : ''; ?>> 193 <summary><h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Diagnostics', 'veloimg' ); ?></h3></summary> 194 <div class="velowp-home-accordion__body"> 195 <div id="velowp-diagnostics"> 196 <?php require VELOWP_DIR . 'templates/admin-diagnostics.php'; ?> 197 </div> 216 198 <table class="form-table" role="presentation"> 217 199 <tr> 218 <th scope="row">< ?php echo esc_html__( 'Frontend delivery', 'veloimg' ); ?></th>200 <th scope="row"><label for="velowp-bypass-query"><?php echo esc_html__( 'View original images', 'veloimg' ); ?></label></th> 219 201 <td> 220 <select name="velowp_settings[delivery_method]" id="velowp-delivery-method"> 221 <?php foreach ( $delivery_labels as $velowp_delivery_key => $velowp_delivery_label ) : ?> 222 <option value="<?php echo esc_attr( $velowp_delivery_key ); ?>" <?php selected( $delivery_method, $velowp_delivery_key ); ?>><?php echo esc_html( (string) $velowp_delivery_label ); ?></option> 223 <?php endforeach; ?> 224 </select> 225 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="off" <?php echo 'off' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Visitors receive original images until frontend delivery is enabled.', 'veloimg' ); ?></p> 226 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="php_safe" <?php echo 'php_safe' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'PHP Safe adds picture/source markup in WordPress. If the layout breaks, you may need to add CSS for picture elements.', 'veloimg' ); ?></p> 227 <p class="description velowp-delivery-method-note" data-velowp-delivery-note="apache" <?php echo 'apache' === $delivery_method ? '' : 'hidden'; ?>><?php echo esc_html__( 'Apache / LiteSpeed keeps HTML unchanged and serves generated AVIF/WebP files through uploads/.htaccess when the browser accepts them.', 'veloimg' ); ?></p> 202 <div class="velowp-bypass-copy"> 203 <input type="text" id="velowp-bypass-query" value="<?php echo esc_attr( $bypass_query ); ?>" class="regular-text" readonly aria-describedby="velowp-bypass-help" /> 204 <button type="button" class="button" id="velowp-bypass-copy"><?php echo esc_html__( 'Copy parameter', 'veloimg' ); ?></button> 205 <span id="velowp-bypass-copy-status" role="status" aria-live="polite" data-success="<?php echo esc_attr__( 'Copied.', 'veloimg' ); ?>" data-fallback="<?php echo esc_attr__( 'Parameter selected. Copy it with Ctrl+C or Command+C.', 'veloimg' ); ?>"></span> 206 </div> 207 <p class="description" id="velowp-bypass-help"><?php echo 'apache' === $delivery_method ? esc_html__( 'Copy the parameter and append it to the JPG/PNG image address to view the original. Saved settings are not changed.', 'veloimg' ) : esc_html__( 'Copy the parameter and append it to the page address to view the original images. Saved settings are not changed.', 'veloimg' ); ?></p> 228 208 </td> 229 209 </tr> 230 210 <tr> 231 <th scope="row">< ?php echo esc_html__( 'Frontend bypass parameter', 'veloimg' ); ?></th>211 <th scope="row"><label for="velowp-log-level"><?php echo esc_html__( 'Log level', 'veloimg' ); ?></label></th> 232 212 <td> 233 <input type="text" name="velowp_settings[bypass_param]" value="<?php echo esc_attr( (string) ( $settings['bypass_param'] ?? 'no_veloimg' ) ); ?>" class="regular-text" /> 234 <p class="description"><?php echo esc_html__( 'Add this query parameter to a page URL to temporarily disable VeloIMG frontend delivery for that page load.', 'veloimg' ); ?></p> 235 </td> 236 </tr> 237 <tr> 238 <th scope="row"><?php echo esc_html__( 'Log level', 'veloimg' ); ?></th> 239 <td> 240 <select name="velowp_settings[log_level]"> 213 <select name="velowp_settings[log_level]" id="velowp-log-level" form="velowp-home-settings-form" aria-describedby="velowp-log-help"> 241 214 <?php $velowp_log_level = (string) ( $settings['log_level'] ?? 'error' ); ?> 242 215 <option value="error" <?php selected( $velowp_log_level, 'error' ); ?>><?php echo esc_html__( 'Error', 'veloimg' ); ?></option> … … 244 217 <option value="info" <?php selected( $velowp_log_level, 'info' ); ?>><?php echo esc_html__( 'Info', 'veloimg' ); ?></option> 245 218 </select> 246 <p class="description" ><?php echo esc_html__( 'Used for diagnostics. Logs are written through error_log() and are visible in wp-content/debug.log when WP_DEBUG_LOG is enabled.', 'veloimg' ); ?></p>219 <p class="description" id="velowp-log-help"><?php echo esc_html__( 'Controls how much diagnostic detail is written to the server error log. Leave Error for normal use; Warning and Info help investigate problems, for example at support request. If WordPress debug logging is enabled, messages appear in wp-content/debug.log.', 'veloimg' ); ?></p> 247 220 </td> 248 221 </tr> 249 222 </table> 250 251 <?php submit_button( __( 'Save settings', 'veloimg' ) ); ?> 252 </form> 223 </div> 224 </details> 225 226 <details class="velowp-card velowp-quick-card velowp-home-accordion velowp-url-audit-card" id="velowp-url-audit"> 227 <summary><h3 class="velowp-quick-card__title"><?php echo esc_html__( 'Check frontend URL', 'veloimg' ); ?></h3></summary> 228 <div class="velowp-home-accordion__body"> 229 <div class="velowp-self-test-head"> 230 <div> 231 <p class="description"><?php echo esc_html__( 'Scan one page from this site and show what visitors actually receive: AVIF, WebP, original fallback, source order or Apache response rewrites.', 'veloimg' ); ?></p> 232 </div> 233 </div> 234 <div class="velowp-url-audit-form"> 235 <input type="url" id="velowp-url-audit-input" class="regular-text" value="<?php echo esc_attr( home_url( '/' ) ); ?>" placeholder="<?php echo esc_attr( home_url( '/' ) ); ?>" /> 236 <button type="button" class="button button-primary velowp-btn-glass velowp-btn-glass--primary" id="velowp-url-audit-run" <?php disabled( 'off' === $delivery_method ); ?>><?php echo esc_html__( 'Check page', 'veloimg' ); ?></button> 237 </div> 238 <p class="description"><?php echo esc_html__( 'Only URLs from the current site are allowed. The check does not start generation and does not change files.', 'veloimg' ); ?></p> 239 <p class="description"><?php echo esc_html__( 'If a page cache is active, clear it after changing frontend delivery; cached HTML can keep old markup until the cache expires.', 'veloimg' ); ?></p> 240 <div class="velowp-url-audit-result" id="velowp-url-audit-result" aria-live="polite"> 241 <p class="description velowp-muted"><?php echo 'off' === $delivery_method ? esc_html__( 'Frontend delivery is disabled. Enable PHP Safe or Apache /.htaccess before running a page delivery check.', 'veloimg' ) : esc_html__( 'No page check has been run yet.', 'veloimg' ); ?></p> 242 </div> 243 </div> 244 </details> 253 245 254 246 <p> -
veloimg/trunk/templates/optimization-home-result.php
r3728637 r3739629 21 21 <div data-velowp-home-summary-group aria-busy="<?php echo $velowp_frozen ? 'true' : 'false'; ?>"> 22 22 <?php if ( $velowp_global ) : ?> 23 <p data-velowp-home-summary="saved"><?php echo esc_html__( 'Bytes saved', 'veloimg' ); ?>: <strong ><?php echo esc_html( $velowp_saved_label ); ?></strong></p>23 <p data-velowp-home-summary="saved"><?php echo esc_html__( 'Bytes saved', 'veloimg' ); ?>: <strong class="velowp-savings-value <?php echo $velowp_saved > 0 ? 'is-success' : ( $velowp_saved < 0 ? 'is-warning' : 'is-neutral' ); ?>"><?php echo esc_html( $velowp_saved_label ); ?></strong></p> 24 24 <?php endif; ?> 25 25 <?php if ( isset( $velowp_global['processed'], $velowp_global['ready'], $velowp_global['failed'] ) ) : ?> -
veloimg/trunk/veloimg.php
r3728637 r3739629 4 4 * Plugin URI: https://veloimg.ru/ 5 5 * Description: Generate WebP and AVIF derivatives, manage large media libraries, and deliver optimized images through PHP Safe or Apache/LiteSpeed rules. 6 * Version: 0.9. 66 * Version: 0.9.7 7 7 * Requires at least: 6.9 8 8 * Requires PHP: 7.4 … … 18 18 } 19 19 20 define( 'VELOWP_VERSION', '0.9. 6' );20 define( 'VELOWP_VERSION', '0.9.7' ); 21 21 define( 'VELOWP_DB_VERSION', '1.5.2' ); 22 22 define( 'VELOWP_FILE', __FILE__ );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)