| @@ -47,8 +47,36 @@ | ||
| 47 | 47 | */ |
| 48 | 48 | const SETTINGS_SLUG = 'gutslider-settings'; |
| 49 | 49 | |
| 50 | 50 | /** |
| 51 | + * Page slug for the Free vs Pro comparison. | |
| 52 | + * | |
| 53 | + * @var string | |
| 54 | + */ | |
| 55 | + const COMPARE_SLUG = 'gutslider-free-vs-pro'; | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * Transient holding the cached content-usage scan. | |
| 59 | + * | |
| 60 | + * @var string | |
| 61 | + */ | |
| 62 | + const USAGE_TRANSIENT = 'gutslider_usage_stats'; | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * Option holding the onboarding checklist state. | |
| 66 | + * | |
| 67 | + * @var string | |
| 68 | + */ | |
| 69 | + const ONBOARDING_OPTION = 'gutslider_onboarding'; | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * Maximum number of posts inspected by the usage scan. | |
| 73 | + * | |
| 74 | + * @var int | |
| 75 | + */ | |
| 76 | + const USAGE_SCAN_LIMIT = 500; | |
| 77 | + | |
| 78 | + /** | |
| 51 | 79 | * Default values for the general settings. |
| 52 | 80 | * |
| 53 | 81 | * @var array<string, mixed> |
| 54 | 82 | */ |
| @@ -66,9 +94,10 @@ | ||
| 66 | 94 | add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 ); |
| 67 | 95 | add_action( 'admin_enqueue_scripts', array( $this, 'admin_assets' ) ); |
| 68 | 96 | add_action( 'rest_api_init', array( $this, 'register_routes' ) ); |
| 69 | 97 | add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) ); |
| 70 | - add_filter( 'admin_footer_text', array( $this, 'admin_footer_text' ) ); | |
| 98 | + add_action( 'save_post', array( self::class, 'flush_usage_cache' ) ); | |
| 99 | + add_action( 'deleted_post', array( self::class, 'flush_usage_cache' ) ); | |
| 71 | 100 | } |
| 72 | 101 | |
| 73 | 102 | /* --------------------------------------------------------------- |
| 74 | 103 | * Settings helpers |
| @@ -219,11 +248,36 @@ | ||
| 219 | 248 | 'manage_options', |
| 220 | 249 | self::SETTINGS_SLUG, |
| 221 | 250 | array( $this, 'render_settings_page' ) |
| 222 | 251 | ); |
| 252 | + | |
| 253 | + if ( self::show_compare() ) { | |
| 254 | + add_submenu_page( | |
| 255 | + self::MENU_SLUG, | |
| 256 | + __( 'Free vs Pro', 'slider-blocks' ), | |
| 257 | + __( 'Free vs Pro', 'slider-blocks' ), | |
| 258 | + 'manage_options', | |
| 259 | + self::COMPARE_SLUG, | |
| 260 | + array( $this, 'render_compare_page' ) | |
| 261 | + ); | |
| 262 | + } | |
| 223 | 263 | } |
| 224 | 264 | |
| 225 | 265 | /** |
| 266 | + * Whether the Free vs Pro page is offered. | |
| 267 | + * | |
| 268 | + * @return bool True while GutSlider Pro isn't active. | |
| 269 | + */ | |
| 270 | + public static function show_compare() { | |
| 271 | + /** | |
| 272 | + * Whether the dashboard offers the Free vs Pro comparison. | |
| 273 | + * | |
| 274 | + * @param bool $show True while GutSlider Pro isn't active. | |
| 275 | + */ | |
| 276 | + return (bool) apply_filters( 'gutslider_show_pro_compare', ! defined( 'GUTSLIDER_PRO_VERSION' ) ); | |
| 277 | + } | |
| 278 | + | |
| 279 | + /** | |
| 226 | 280 | * Screen IDs that should receive the dashboard assets. |
| 227 | 281 | * |
| 228 | 282 | * @return array<int, string> Screen IDs. |
| 229 | 283 | */ |
| @@ -231,8 +285,9 @@ | ||
| 231 | 285 | return array( |
| 232 | 286 | 'toplevel_page_' . self::MENU_SLUG, |
| 233 | 287 | 'gutslider_page_' . self::BLOCKS_SLUG, |
| 234 | 288 | 'gutslider_page_' . self::SETTINGS_SLUG, |
| 289 | + 'gutslider_page_' . self::COMPARE_SLUG, | |
| 235 | 290 | 'gutslider_page_gutslider-license', |
| 236 | 291 | ); |
| 237 | 292 | } |
| 238 | 293 | |
| @@ -298,8 +353,13 @@ | ||
| 298 | 353 | 'changePlural' => __( '%d unsaved changes', 'slider-blocks' ), |
| 299 | 354 | 'leaveWarning' => __( 'You have unsaved changes.', 'slider-blocks' ), |
| 300 | 355 | /* translators: 1: enabled block count, 2: total block count. */ |
| 301 | 356 | 'enabledCount' => __( '%1$d of %2$d enabled', 'slider-blocks' ), |
| 357 | + /* translators: 1: free block count, 2: pro block count, 3: enabled count, 4: total count. */ | |
| 358 | + 'blockSummary' => __( '%1$d free blocks · %2$d pro blocks · %3$d of %4$d enabled', 'slider-blocks' ), | |
| 359 | + 'allEnabled' => __( 'All blocks enabled', 'slider-blocks' ), | |
| 360 | + /* translators: %d: number of disabled blocks. */ | |
| 361 | + 'someDisabled' => __( '%d turned off', 'slider-blocks' ), | |
| 302 | 362 | ), |
| 303 | 363 | ) |
| 304 | 364 | ); |
| 305 | 365 | } |
| @@ -317,26 +377,441 @@ | ||
| 317 | 377 | |
| 318 | 378 | return $classes; |
| 319 | 379 | } |
| 320 | 380 | |
| 381 | + /* --------------------------------------------------------------- | |
| 382 | + * Dashboard data | |
| 383 | + * ------------------------------------------------------------- */ | |
| 384 | + | |
| 321 | 385 | /** |
| 322 | - * Replace the admin footer credit on dashboard screens. | |
| 386 | + * Scan post content for GutSlider blocks. | |
| 323 | 387 | * |
| 324 | - * @param string $text Default footer text. | |
| 325 | - * @return string Filtered footer text. | |
| 388 | + * Counts how many slider blocks are in use and how many posts they | |
| 389 | + * live on. The result is cached because it is a full-text scan. | |
| 390 | + * | |
| 391 | + * @return array{sliders:int, posts:int, capped:bool} Usage figures. | |
| 326 | 392 | */ |
| 327 | - public function admin_footer_text( $text ) { | |
| 328 | - if ( ! self::is_dashboard_screen() ) { | |
| 329 | - return $text; | |
| 393 | + public static function get_usage_stats() { | |
| 394 | + $cached = get_transient( self::USAGE_TRANSIENT ); | |
| 395 | + | |
| 396 | + if ( is_array( $cached ) ) { | |
| 397 | + return $cached; | |
| 330 | 398 | } |
| 331 | 399 | |
| 400 | + global $wpdb; | |
| 401 | + | |
| 402 | + $needle = '<!-- wp:gutsliders/'; | |
| 403 | + $like = '%' . $wpdb->esc_like( $needle ) . '%'; | |
| 404 | + | |
| 405 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Content scan, cached in a transient below. | |
| 406 | + $contents = $wpdb->get_col( | |
| 407 | + $wpdb->prepare( | |
| 408 | + "SELECT post_content FROM {$wpdb->posts} | |
| 409 | + WHERE post_type NOT IN ( 'revision', 'nav_menu_item' ) | |
| 410 | + AND post_status NOT IN ( 'auto-draft', 'trash', 'inherit' ) | |
| 411 | + AND post_content LIKE %s | |
| 412 | + LIMIT %d", | |
| 413 | + $like, | |
| 414 | + self::USAGE_SCAN_LIMIT | |
| 415 | + ) | |
| 416 | + ); | |
| 417 | + | |
| 418 | + $contents = is_array( $contents ) ? $contents : array(); | |
| 419 | + $sliders = 0; | |
| 420 | + | |
| 421 | + foreach ( $contents as $content ) { | |
| 422 | + $sliders += substr_count( (string) $content, $needle ); | |
| 423 | + } | |
| 424 | + | |
| 425 | + $stats = array( | |
| 426 | + 'sliders' => $sliders, | |
| 427 | + 'posts' => count( $contents ), | |
| 428 | + 'capped' => count( $contents ) >= self::USAGE_SCAN_LIMIT, | |
| 429 | + ); | |
| 430 | + | |
| 431 | + set_transient( self::USAGE_TRANSIENT, $stats, 12 * HOUR_IN_SECONDS ); | |
| 432 | + | |
| 433 | + return $stats; | |
| 434 | + } | |
| 435 | + | |
| 436 | + /** | |
| 437 | + * Drop the cached usage scan. | |
| 438 | + * | |
| 439 | + * @return void | |
| 440 | + */ | |
| 441 | + public static function flush_usage_cache() { | |
| 442 | + delete_transient( self::USAGE_TRANSIENT ); | |
| 443 | + } | |
| 444 | + | |
| 445 | + /** | |
| 446 | + * Measure the stylesheets GutSlider has generated. | |
| 447 | + * | |
| 448 | + * @return array{files:int, bytes:int, average:int} Stylesheet figures. | |
| 449 | + */ | |
| 450 | + public static function get_css_stats() { | |
| 451 | + $upload = wp_upload_dir(); | |
| 452 | + $dir = trailingslashit( $upload['basedir'] ) . 'gutslider-styles'; | |
| 453 | + $files = is_dir( $dir ) ? glob( $dir . '/*.css' ) : array(); | |
| 454 | + $files = is_array( $files ) ? $files : array(); | |
| 455 | + $bytes = 0; | |
| 456 | + | |
| 457 | + foreach ( $files as $file ) { | |
| 458 | + $bytes += (int) filesize( $file ); | |
| 459 | + } | |
| 460 | + | |
| 461 | + $count = count( $files ); | |
| 462 | + | |
| 463 | + return array( | |
| 464 | + 'files' => $count, | |
| 465 | + 'bytes' => $bytes, | |
| 466 | + 'average' => $count > 0 ? (int) round( $bytes / $count ) : 0, | |
| 467 | + ); | |
| 468 | + } | |
| 469 | + | |
| 470 | + /** | |
| 471 | + * Get the Pro licence status. | |
| 472 | + * | |
| 473 | + * The free plugin has no licence data of its own, so the Pro plugin | |
| 474 | + * supplies it through the `gutslider_license_status` filter. Anything | |
| 475 | + * missing simply degrades to a quieter pill. | |
| 476 | + * | |
| 477 | + * @return array{state:string, label:string}|null Licence data, or null when unavailable. | |
| 478 | + */ | |
| 479 | + public static function get_license() { | |
| 480 | + if ( ! defined( 'GUTSLIDER_PRO_VERSION' ) ) { | |
| 481 | + return null; | |
| 482 | + } | |
| 483 | + | |
| 484 | + /** | |
| 485 | + * Filter the licence status shown in the dashboard header. | |
| 486 | + * | |
| 487 | + * @param array|null $status Associative array with `state` (active|expiring|inactive) | |
| 488 | + * and either a ready-made `label` or an `expires` timestamp. | |
| 489 | + */ | |
| 490 | + $status = apply_filters( 'gutslider_license_status', null ); | |
| 491 | + | |
| 492 | + if ( ! is_array( $status ) ) { | |
| 493 | + return array( | |
| 494 | + 'state' => 'active', | |
| 495 | + 'label' => __( 'GutSlider Pro active', 'slider-blocks' ), | |
| 496 | + ); | |
| 497 | + } | |
| 498 | + | |
| 499 | + $state = isset( $status['state'] ) ? (string) $status['state'] : 'active'; | |
| 500 | + $label = isset( $status['label'] ) ? (string) $status['label'] : ''; | |
| 501 | + | |
| 502 | + if ( '' === $label && ! empty( $status['expires'] ) ) { | |
| 503 | + $expires = is_numeric( $status['expires'] ) ? (int) $status['expires'] : strtotime( (string) $status['expires'] ); | |
| 504 | + | |
| 505 | + if ( $expires ) { | |
| 506 | + $label = sprintf( | |
| 507 | + /* translators: %s: licence renewal date. */ | |
| 508 | + 'expiring' === $state ? __( 'License expires %s', 'slider-blocks' ) : __( 'License active · renews %s', 'slider-blocks' ), | |
| 509 | + date_i18n( 'j M Y', $expires ) | |
| 510 | + ); | |
| 511 | + } | |
| 512 | + } | |
| 513 | + | |
| 514 | + if ( '' === $label ) { | |
| 515 | + $label = 'inactive' === $state | |
| 516 | + ? __( 'License inactive', 'slider-blocks' ) | |
| 517 | + : __( 'GutSlider Pro active', 'slider-blocks' ); | |
| 518 | + } | |
| 519 | + | |
| 520 | + return array( | |
| 521 | + 'state' => in_array( $state, array( 'active', 'expiring', 'inactive' ), true ) ? $state : 'active', | |
| 522 | + 'label' => $label, | |
| 523 | + ); | |
| 524 | + } | |
| 525 | + | |
| 526 | + /** | |
| 527 | + * Get the banner shown across the top of the dashboard. | |
| 528 | + * | |
| 529 | + * The free plugin owns the markup and styling; anything with | |
| 530 | + * something to say — the Pro plugin's licence prompt, for example — | |
| 531 | + * supplies the copy through the filter. | |
| 532 | + * | |
| 533 | + * @return array{tone:string, title:string, text:string, action_label:string, action_url:string}|null | |
| 534 | + * Normalised notice, or null when there is nothing to show. | |
| 535 | + */ | |
| 536 | + public static function get_dashboard_notice() { | |
| 537 | + /** | |
| 538 | + * Filter the dashboard banner. | |
| 539 | + * | |
| 540 | + * Return an empty array for no banner, or an array with: | |
| 541 | + * `tone` (info|warning|error), `title`, `text`, `action_label` | |
| 542 | + * and `action_url`. A notice without a title is discarded. | |
| 543 | + * | |
| 544 | + * @param array $notice Notice definition. | |
| 545 | + */ | |
| 546 | + $notice = apply_filters( 'gutslider_dashboard_notice', array() ); | |
| 547 | + | |
| 548 | + if ( ! is_array( $notice ) || empty( $notice['title'] ) ) { | |
| 549 | + return null; | |
| 550 | + } | |
| 551 | + | |
| 552 | + $tone = isset( $notice['tone'] ) ? (string) $notice['tone'] : 'info'; | |
| 553 | + | |
| 554 | + return array( | |
| 555 | + 'tone' => in_array( $tone, array( 'info', 'warning', 'error' ), true ) ? $tone : 'info', | |
| 556 | + 'title' => (string) $notice['title'], | |
| 557 | + 'text' => isset( $notice['text'] ) ? (string) $notice['text'] : '', | |
| 558 | + 'action_label' => isset( $notice['action_label'] ) ? (string) $notice['action_label'] : '', | |
| 559 | + 'action_url' => isset( $notice['action_url'] ) ? (string) $notice['action_url'] : '', | |
| 560 | + ); | |
| 561 | + } | |
| 562 | + | |
| 563 | + /** | |
| 564 | + * Read the current release notes out of readme.txt. | |
| 565 | + * | |
| 566 | + * @param int $limit Maximum number of entries to return. | |
| 567 | + * @return array<int, array{tag:string, label:string, text:string}> Release notes. | |
| 568 | + */ | |
| 569 | + public static function get_changelog( $limit = 4 ) { | |
| 570 | + $readme = GUTSLIDER_DIR . '/readme.txt'; | |
| 571 | + | |
| 572 | + if ( ! is_readable( $readme ) ) { | |
| 573 | + return array(); | |
| 574 | + } | |
| 575 | + | |
| 576 | + $contents = file_get_contents( $readme ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local plugin file. | |
| 577 | + | |
| 578 | + if ( ! is_string( $contents ) ) { | |
| 579 | + return array(); | |
| 580 | + } | |
| 581 | + | |
| 582 | + // Grab the block of lines under the heading for the running version. | |
| 583 | + $pattern = '/^=\s*' . preg_quote( GUTSLIDER_VERSION, '/' ) . '\s*=\s*$(.*?)(?=^=\s|\z)/ms'; | |
| 584 | + | |
| 585 | + if ( ! preg_match( $pattern, $contents, $matches ) ) { | |
| 586 | + return array(); | |
| 587 | + } | |
| 588 | + | |
| 589 | + $tags = array( | |
| 590 | + 'added' => 'new', | |
| 591 | + 'new' => 'new', | |
| 592 | + 'fixed' => 'fixed', | |
| 593 | + 'fix' => 'fixed', | |
| 594 | + 'improved' => 'improved', | |
| 595 | + 'updated' => 'improved', | |
| 596 | + 'optimized' => 'improved', | |
| 597 | + 'enhanced' => 'improved', | |
| 598 | + 'changed' => 'improved', | |
| 599 | + 'checked' => 'improved', | |
| 600 | + ); | |
| 601 | + | |
| 602 | + $labels = array( | |
| 603 | + 'new' => __( 'New', 'slider-blocks' ), | |
| 604 | + 'improved' => __( 'Improved', 'slider-blocks' ), | |
| 605 | + 'fixed' => __( 'Fixed', 'slider-blocks' ), | |
| 606 | + ); | |
| 607 | + | |
| 608 | + $entries = array(); | |
| 609 | + | |
| 610 | + foreach ( preg_split( '/\R/', $matches[1] ) as $line ) { | |
| 611 | + $line = trim( $line ); | |
| 612 | + | |
| 613 | + if ( '' === $line || '*' !== $line[0] ) { | |
| 614 | + continue; | |
| 615 | + } | |
| 616 | + | |
| 617 | + $text = trim( ltrim( $line, '*' ) ); | |
| 618 | + $tag = 'new'; | |
| 619 | + | |
| 620 | + if ( preg_match( '/^([A-Za-z]+)\s*:\s*(.+)$/', $text, $prefix ) ) { | |
| 621 | + $key = strtolower( $prefix[1] ); | |
| 622 | + | |
| 623 | + if ( isset( $tags[ $key ] ) ) { | |
| 624 | + $tag = $tags[ $key ]; | |
| 625 | + $text = trim( $prefix[2] ); | |
| 626 | + } | |
| 627 | + } else { | |
| 628 | + $first = strtolower( strtok( $text, ' ' ) ); | |
| 629 | + $tag = isset( $tags[ $first ] ) ? $tags[ $first ] : 'new'; | |
| 630 | + } | |
| 631 | + | |
| 632 | + $entries[] = array( | |
| 633 | + 'tag' => $tag, | |
| 634 | + 'label' => $labels[ $tag ], | |
| 635 | + 'text' => $text, | |
| 636 | + ); | |
| 637 | + | |
| 638 | + if ( count( $entries ) >= $limit ) { | |
| 639 | + break; | |
| 640 | + } | |
| 641 | + } | |
| 642 | + | |
| 643 | + return $entries; | |
| 644 | + } | |
| 645 | + | |
| 646 | + /** | |
| 647 | + * Mark an onboarding step as done. | |
| 648 | + * | |
| 649 | + * @param string $step Step key. | |
| 650 | + * @return void | |
| 651 | + */ | |
| 652 | + public static function complete_onboarding_step( $step ) { | |
| 653 | + $state = get_option( self::ONBOARDING_OPTION, array() ); | |
| 654 | + | |
| 655 | + if ( ! is_array( $state ) ) { | |
| 656 | + $state = array(); | |
| 657 | + } | |
| 658 | + | |
| 659 | + if ( empty( $state[ $step ] ) ) { | |
| 660 | + $state[ $step ] = true; | |
| 661 | + update_option( self::ONBOARDING_OPTION, $state ); | |
| 662 | + } | |
| 663 | + } | |
| 664 | + | |
| 665 | + /** | |
| 666 | + * Whether an onboarding step has been completed. | |
| 667 | + * | |
| 668 | + * @param string $step Step key. | |
| 669 | + * @return bool True when done. | |
| 670 | + */ | |
| 671 | + public static function onboarding_done( $step ) { | |
| 672 | + $state = get_option( self::ONBOARDING_OPTION, array() ); | |
| 673 | + | |
| 674 | + return is_array( $state ) && ! empty( $state[ $step ] ); | |
| 675 | + } | |
| 676 | + | |
| 677 | + /* --------------------------------------------------------------- | |
| 678 | + * Icons | |
| 679 | + * ------------------------------------------------------------- */ | |
| 680 | + | |
| 681 | + /** | |
| 682 | + * Inline SVG bodies, drawn on a 24x24 grid with a 1.7px stroke. | |
| 683 | + * | |
| 684 | + * @return array<string, string> Map of icon name to SVG children. | |
| 685 | + */ | |
| 686 | + private static function icon_paths() { | |
| 687 | + static $paths = null; | |
| 688 | + | |
| 689 | + if ( null !== $paths ) { | |
| 690 | + return $paths; | |
| 691 | + } | |
| 692 | + | |
| 693 | + $paths = array( | |
| 694 | + 'sun' => '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>', | |
| 695 | + 'moon' => '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>', | |
| 696 | + 'book-open' => '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>', | |
| 697 | + 'plus' => '<path d="M5 12h14M12 5v14"/>', | |
| 698 | + 'sliders-horizontal' => '<path d="M21 4h-7M10 4H3M21 12h-9M8 12H3M21 20h-5M12 20H3M14 2v4M8 10v4M16 18v4"/>', | |
| 699 | + 'layout-grid' => '<rect width="7" height="7" x="3" y="3" rx="1.5"/><rect width="7" height="7" x="14" y="3" rx="1.5"/><rect width="7" height="7" x="14" y="14" rx="1.5"/><rect width="7" height="7" x="3" y="14" rx="1.5"/>', | |
| 700 | + 'layers' => '<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>', | |
| 701 | + 'gauge' => '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>', | |
| 702 | + 'type' => '<path d="M4 7V4h16v3M9 20h6M12 4v16"/>', | |
| 703 | + 'search' => '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>', | |
| 704 | + 'arrow-right' => '<path d="M5 12h14M12 5l7 7-7 7"/>', | |
| 705 | + 'check' => '<path d="M20 6 9 17l-5-5"/>', | |
| 706 | + 'play' => '<path d="M7 4.5v15l13-7.5z" fill="currentColor" stroke="none"/>', | |
| 707 | + 'life-buoy' => '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/><path d="m4.93 4.93 4.24 4.24M14.83 9.17l4.24-4.24M14.83 14.83l4.24 4.24M9.17 14.83l-4.24 4.24"/>', | |
| 708 | + 'star' => '<path d="m12 2.6 2.9 5.9 6.5.9-4.7 4.6 1.1 6.4-5.8-3-5.8 3 1.1-6.4L2.6 9.4l6.5-.9z"/>', | |
| 709 | + 'chevron-right' => '<path d="m9 18 6-6-6-6"/>', | |
| 710 | + 'layout-template' => '<rect width="18" height="7" x="3" y="3" rx="1.5"/><rect width="9" height="7" x="3" y="14" rx="1.5"/><rect width="5" height="7" x="16" y="14" rx="1.5"/>', | |
| 711 | + 'chevrons-down-up' => '<path d="m7 20 5-5 5 5M7 4l5 5 5-5"/>', | |
| 712 | + 'map-pin' => '<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"/><circle cx="12" cy="10" r="3"/>', | |
| 713 | + 'lock' => '<rect width="16" height="11" x="4" y="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>', | |
| 714 | + 'key' => '<circle cx="7.5" cy="15.5" r="4.5"/><path d="m10.7 12.3 9.3-9.3M17 5l2.5 2.5M14 8l2.5 2.5"/>', | |
| 715 | + 'alert-triangle' => '<path d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4M12 17h.01"/>', | |
| 716 | + 'external' => '<path d="M14 4h6v6M20 4l-8 8M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>', | |
| 717 | + | |
| 718 | + /* Block glyphs. */ | |
| 719 | + 'gallery-horizontal' => '<path d="M2 7v10M6 5v14"/><rect width="12" height="18" x="10" y="3" rx="2"/>', | |
| 720 | + 'move-horizontal' => '<path d="m18 8 4 4-4 4M2 12h20M6 8l-4 4 4 4"/>', | |
| 721 | + 'quote' => '<path d="M10 11H6.5A2.5 2.5 0 0 1 4 8.5v-.5A3 3 0 0 1 7 5h1M10 11v3a5 5 0 0 1-5 5"/><path d="M21 11h-3.5A2.5 2.5 0 0 1 15 8.5v-.5a3 3 0 0 1 3-3h1M21 11v3a5 5 0 0 1-5 5"/>', | |
| 722 | + 'images' => '<rect width="16" height="16" x="6" y="2" rx="2"/><path d="M18 22H4a2 2 0 0 1-2-2V6"/><circle cx="12" cy="8" r="1.6"/><path d="m22 13-1.3-1.3a2.4 2.4 0 0 0-3.4 0L11 18"/>', | |
| 723 | + 'image' => '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="1.8"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>', | |
| 724 | + 'video' => '<path d="m22 8-6 4 6 4z"/><rect width="14" height="12" x="2" y="6" rx="2"/>', | |
| 725 | + 'newspaper' => '<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2"/><path d="M18 14h-8M15 18h-5M10 6h8v4h-8z"/>', | |
| 726 | + 'gallery-thumbnails' => '<rect width="18" height="14" x="3" y="3" rx="2"/><path d="M4 21h1M9.5 21h1M15 21h1M20 21h1"/>', | |
| 727 | + 'columns' => '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>', | |
| 728 | + 'blinds' => '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18M15 3v18"/>', | |
| 729 | + 'sparkles' => '<path d="m12 3 1.9 5 5.1 1.9-5.1 1.9L12 17l-1.9-5.2L5 9.9 10.1 8z"/><path d="M19 15.5 19.7 18l2.3.8-2.3.8L19 22l-.7-2.4-2.3-.8 2.3-.8z"/>', | |
| 730 | + 'scissors' => '<circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.12 8.12 20 20M20 4 8.12 15.88"/>', | |
| 731 | + 'shirt' => '<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"/>', | |
| 732 | + 'rows' => '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3M21 15H3"/>', | |
| 733 | + 'box' => '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m3.3 7 8.7 5 8.7-5M12 22V12"/>', | |
| 734 | + 'panorama' => '<rect width="20" height="12" x="2" y="6" rx="2"/><path d="m6 14 3-3 3 3 3-4 3 4"/>', | |
| 735 | + 'credit-card' => '<rect width="20" height="14" x="2" y="5" rx="2"/><path d="M2 10h20"/>', | |
| 736 | + 'heart' => '<path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/>', | |
| 737 | + 'pointer' => '<path d="m3.7 3.04 6.5 16a.5.5 0 0 0 .95-.06l1.57-6.09a2 2 0 0 1 1.44-1.48l6.13-1.58a.5.5 0 0 0 .06-.94l-16-6.5a.5.5 0 0 0-.65.65z"/>', | |
| 738 | + 'shopping-bag' => '<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18M16 10a4 4 0 0 1-8 0"/>', | |
| 739 | + 'tags' => '<path d="M12 2H2v10l9.29 9.29a2 2 0 0 0 2.83 0l6.58-6.58a2 2 0 0 0 0-2.83z"/><path d="M7 7h.01"/>', | |
| 740 | + ); | |
| 741 | + | |
| 742 | + return $paths; | |
| 743 | + } | |
| 744 | + | |
| 745 | + /** | |
| 746 | + * Render an inline icon. | |
| 747 | + * | |
| 748 | + * @param string $name Icon name. | |
| 749 | + * @param string $classes Additional classes. | |
| 750 | + * @return string SVG markup, or an empty string for unknown icons. | |
| 751 | + */ | |
| 752 | + public static function get_icon( $name, $classes = '' ) { | |
| 753 | + $paths = self::icon_paths(); | |
| 754 | + | |
| 755 | + if ( ! isset( $paths[ $name ] ) ) { | |
| 756 | + return ''; | |
| 757 | + } | |
| 758 | + | |
| 332 | 759 | return sprintf( |
| 333 | - /* translators: %s: link to the review page. */ | |
| 334 | - esc_html__( 'Enjoying GutSlider? %s on WordPress.org.', 'slider-blocks' ), | |
| 335 | - '<a href="https://wordpress.org/support/plugin/slider-blocks/reviews/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Leave a review', 'slider-blocks' ) . '</a>' | |
| 760 | + '<svg class="gs-icon%s" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">%s</svg>', | |
| 761 | + $classes ? ' ' . esc_attr( $classes ) : '', | |
| 762 | + $paths[ $name ] | |
| 336 | 763 | ); |
| 337 | 764 | } |
| 338 | 765 | |
| 766 | + /** | |
| 767 | + * Echo an inline icon. | |
| 768 | + * | |
| 769 | + * @param string $name Icon name. | |
| 770 | + * @param string $classes Additional classes. | |
| 771 | + * @return void | |
| 772 | + */ | |
| 773 | + public static function icon( $name, $classes = '' ) { | |
| 774 | + echo self::get_icon( $name, $classes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static inline SVG. | |
| 775 | + } | |
| 776 | + | |
| 777 | + /** | |
| 778 | + * Map a block to its glyph. | |
| 779 | + * | |
| 780 | + * @param string $block_name Block directory name. | |
| 781 | + * @return string Icon name. | |
| 782 | + */ | |
| 783 | + public static function block_icon( $block_name ) { | |
| 784 | + $map = array( | |
| 785 | + 'content-slider' => 'gallery-horizontal', | |
| 786 | + 'marquee' => 'move-horizontal', | |
| 787 | + 'any-content' => 'layout-template', | |
| 788 | + 'testimonial-slider' => 'quote', | |
| 789 | + 'photo-carousel' => 'images', | |
| 790 | + 'logo-carousel' => 'gallery-thumbnails', | |
| 791 | + 'before-after' => 'columns', | |
| 792 | + 'videos-carousel' => 'video', | |
| 793 | + 'post-slider' => 'newspaper', | |
| 794 | + 'shader-slider' => 'sparkles', | |
| 795 | + 'shutters-slider' => 'blinds', | |
| 796 | + 'slicer-slider' => 'scissors', | |
| 797 | + 'fashion-slider' => 'shirt', | |
| 798 | + 'triple-slider' => 'rows', | |
| 799 | + 'spring-carousel' => 'box', | |
| 800 | + 'panorama-carousel' => 'panorama', | |
| 801 | + 'three-d-carousel' => 'box', | |
| 802 | + 'card-slider' => 'credit-card', | |
| 803 | + 'marquee-carousel' => 'move-horizontal', | |
| 804 | + 'material-carousel' => 'layout-grid', | |
| 805 | + 'tinder-slider' => 'heart', | |
| 806 | + 'hover-slider' => 'pointer', | |
| 807 | + 'product-carousel' => 'shopping-bag', | |
| 808 | + 'product-categories-carousel' => 'tags', | |
| 809 | + ); | |
| 810 | + | |
| 811 | + return isset( $map[ $block_name ] ) ? $map[ $block_name ] : 'layers'; | |
| 812 | + } | |
| 813 | + | |
| 339 | 814 | /* --------------------------------------------------------------- |
| 340 | 815 | * Views |
| 341 | 816 | * ------------------------------------------------------------- */ |
| 342 | 817 | |
| @@ -373,12 +848,16 @@ | ||
| 373 | 848 | |
| 374 | 849 | $this->view( |
| 375 | 850 | 'dashboard', |
| 376 | 851 | array( |
| 377 | - 'blocks' => $blocks, | |
| 378 | - 'total' => count( $blocks ), | |
| 379 | - 'enabled' => self::count_enabled( $blocks ), | |
| 380 | - 'current' => self::MENU_SLUG, | |
| 852 | + 'blocks' => $blocks, | |
| 853 | + 'total' => count( $blocks ), | |
| 854 | + 'enabled' => self::count_enabled( $blocks ), | |
| 855 | + 'current' => self::MENU_SLUG, | |
| 856 | + 'settings' => self::get_settings(), | |
| 857 | + 'usage' => self::get_usage_stats(), | |
| 858 | + 'css_stats' => self::get_css_stats(), | |
| 859 | + 'changelog' => self::get_changelog(), | |
| 381 | 860 | ) |
| 382 | 861 | ); |
| 383 | 862 | } |
| 384 | 863 | |
| @@ -401,8 +880,23 @@ | ||
| 401 | 880 | ); |
| 402 | 881 | } |
| 403 | 882 | |
| 404 | 883 | /** |
| 884 | + * Render the Free vs Pro screen. | |
| 885 | + * | |
| 886 | + * @return void | |
| 887 | + */ | |
| 888 | + public function render_compare_page() { | |
| 889 | + $this->view( | |
| 890 | + 'compare-page', | |
| 891 | + array( | |
| 892 | + 'blocks' => $this->get_blocks_with_state(), | |
| 893 | + 'current' => self::COMPARE_SLUG, | |
| 894 | + ) | |
| 895 | + ); | |
| 896 | + } | |
| 897 | + | |
| 898 | + /** | |
| 405 | 899 | * Render the settings screen. |
| 406 | 900 | * |
| 407 | 901 | * @return void |
| 408 | 902 | */ |
| @@ -532,8 +1026,9 @@ | ||
| 532 | 1026 | $saved[ $name ] = $enabled; |
| 533 | 1027 | } |
| 534 | 1028 | |
| 535 | 1029 | $this->sync_block_list( $saved ); |
| 1030 | + self::complete_onboarding_step( 'blocks' ); | |
| 536 | 1031 | |
| 537 | 1032 | return rest_ensure_response( |
| 538 | 1033 | array( |
| 539 | 1034 | 'success' => true, |
| @@ -611,8 +1106,9 @@ | ||
| 611 | 1106 | $settings['remove_data'] = rest_sanitize_boolean( $incoming['remove_data'] ); |
| 612 | 1107 | } |
| 613 | 1108 | |
| 614 | 1109 | update_option( self::SETTINGS_OPTION, $settings ); |
| 1110 | + self::complete_onboarding_step( 'settings' ); | |
| 615 | 1111 | |
| 616 | 1112 | /* |
| 617 | 1113 | * Switching to inline delivery leaves the previously generated |
| 618 | 1114 | * stylesheets orphaned in the uploads directory, so clear them out. |