PluginProbe
GutSlider – All in One Slider and Carousel Blocks for Gutenberg / 3.2.0
GutSlider – All in One Slider and Carousel Blocks for Gutenberg v3.2.0
3.2.0 3.1.0 3.0.0 2.13.2 2.13.1 2.13.0 trunk 1.0.0 2.1.0 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.1 2.5.3 2.5.4 2.5.5 All 57 releases
← All changes | admin/admin.php +510 -14 3.0.0 → 3.2.0 View file →
@@ -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.