>
1100, 'sidebar' => 260, 'gap' => 20, ); } /** * Inline style string for #afsr-admin-app CSS variables (first paint). * * @since 5.4.5 * * @return string */ public static function support_app_inline_style() { $t = self::support_layout_tokens(); return sprintf( '--afsr-main-max:%dpx;--afsr-sidebar-width:%dpx;--afsr-layout-gap:%dpx;--afsr-prelayout-max:min(%dpx,calc(100%% - %dpx));max-width:100%%;width:100%%;box-sizing:border-box;', $t['main_max'], $t['sidebar'], $t['gap'], $t['main_max'], $t['sidebar'] + $t['gap'] ); } /** * Open the two-column content shell. * * Sidebar is emitted immediately (column 2) so license notices / tables * streaming into the main column cannot paint over an empty rail. * Call {@see support_layout_close()} after main column markup. * * @since 5.4.5 */ public static function support_layout_open() { $t = self::support_layout_tokens(); $layout_style = sprintf( 'display:grid;grid-template-columns:minmax(0,%dpx) %dpx;justify-content:space-between;column-gap:%dpx;row-gap:%dpx;align-items:start;width:100%%;box-sizing:border-box;', $t['main_max'], $t['sidebar'], $t['gap'], $t['gap'] ); $main_style = sprintf( 'grid-column:1;grid-row:1;min-width:0;width:100%%;max-width:%dpx;box-sizing:border-box;overflow-x:clip;', $t['main_max'] ); printf( '
', esc_attr( $layout_style ) ); self::render_support_sidebar(); printf( '
', esc_attr( $main_style ) ); } /** * Close the main column and the support shell. * * @since 5.4.5 */ public static function support_layout_close() { echo '
'; echo '
'; } /** * Right-rail "Report an issue" card (Import + Settings). * * Lite → WordPress.org support. Pro plugin present → vendor contact form. * * @since 5.4.5 */ public static function render_support_sidebar() { $t = self::support_layout_tokens(); $support_url = self::is_pro_plugin_present() ? 'https://elearningevolve.com/contact/' : 'https://wordpress.org/support/plugin/add-from-server-reloaded/'; $aside_style = sprintf( 'grid-column:2;grid-row:1;width:%dpx;max-width:%dpx;box-sizing:border-box;', $t['sidebar'], $t['sidebar'] ); ?> .afsr-page-header,#afsr-admin-app.afsrreloaded-wrap>.afsr-pro-banner,#afsr-admin-app.afsr-settings-wrap>.afsr-page-header,#afsr-admin-app .afsr-stepper{max-width:var(--afsr-prelayout-max)}' . '@media (max-width:960px){#afsr-admin-app.afsrreloaded-wrap,#afsr-admin-app.afsr-settings-wrap{--afsr-prelayout-max:100%%}#afsr-admin-app .afsr-browse-layout{grid-template-columns:minmax(0,1fr)!important}#afsr-admin-app .afsr-browse-main,#afsr-admin-app .afsr-support-sidebar{grid-column:1!important;grid-row:auto!important;max-width:100%%!important;width:100%%!important}}', $t['main_max'], $t['sidebar'], $t['gap'] ); wp_enqueue_style( 'common' ); wp_add_inline_style( 'common', $css ); } /** * Enqueue Import UI assets for locked Pro screens / Settings. * * @since 5.4.3 */ public static function enqueue_locked_ui_assets() { $wizard_css = AFSRRELOADED_PLUGIN_DIR_PATH . 'assets/css/admin-styles.css'; $wizard_js = AFSRRELOADED_PLUGIN_DIR_PATH . 'assets/js/admin-scripts.js'; if ( ! wp_style_is( 'afsr-admin-ui', 'registered' ) ) { wp_register_style( 'afsr-admin-ui', plugins_url( 'assets/css/admin-styles.css', AFSRRELOADED_PLUGIN_FILE ), array( 'common', 'dashicons', 'add-from-server-reloaded' ), file_exists( $wizard_css ) ? (string) filemtime( $wizard_css ) : AFSRRELOADED_VERSION ); } if ( ! wp_script_is( 'afsr-admin-ui', 'registered' ) ) { wp_register_script( 'afsr-admin-ui', plugins_url( 'assets/js/admin-scripts.js', AFSRRELOADED_PLUGIN_FILE ), array( 'jquery', 'add-from-server-reloaded' ), file_exists( $wizard_js ) ? (string) filemtime( $wizard_js ) : AFSRRELOADED_VERSION, true ); } self::enqueue_browse_layout_critical_css(); wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'add-from-server-reloaded' ); wp_enqueue_style( 'afsr-admin-ui' ); wp_enqueue_script( 'add-from-server-reloaded' ); wp_enqueue_script( 'afsr-admin-ui' ); } /** * Sidebar PRO badge / Get Pro styling (keeps default WP admin fonts). * * Must load on every wp-admin screen: submenu labels include the badge * HTML globally, so gating to plugin pages made "PRO" render as plain * text on Dashboard and other non-plugin screens. * CSS is scoped to #adminmenu only. * * @since 5.4.3 * * @param string $hook Current admin page hook (unused). */ public static function enqueue_menu_badge_assets( $hook = '' ) { unset( $hook ); if ( ! is_admin() ) { return; } // No Lite PRO badges when Pro is active — skip the tiny stylesheet. if ( self::is_pro_active() ) { return; } $css = ' #adminmenu .afsr-menu-pro-badge { display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 999px; background: #8a6d3b; color: #f5e6b8; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; line-height: 1.5; vertical-align: middle; } #adminmenu .wp-submenu a[href*="elearningevolve.com/products/add-from-server-pro"] { color: #e8c56a !important; } #adminmenu .afsr-menu-external { margin-left: 4px; font-size: 12px; } '; wp_register_style( 'afsr-admin-menu-badges', false, array(), AFSRRELOADED_VERSION ); wp_enqueue_style( 'afsr-admin-menu-badges' ); wp_add_inline_style( 'afsr-admin-menu-badges', $css ); } /** * Add PRO badges to Free submenu labels and normalize order. * * Lite: Settings is 2nd (after the main Import item). * Pro: Settings sits after Access Control (feature menus first). * * @since 5.4.3 */ public static function decorate_submenu_labels() { global $submenu; if ( empty( $submenu['add-from-server-reloaded'] ) || ! is_array( $submenu['add-from-server-reloaded'] ) ) { return; } $pro_active = self::is_pro_active(); $top_label = $pro_active ? __( 'AFS Pro', 'add-from-server-reloaded' ) : __( 'AFS Lite', 'add-from-server-reloaded' ); // Keep the auto first submenu item label in sync with the top-level menu name. foreach ( $submenu['add-from-server-reloaded'] as $index => $item ) { if ( ! is_array( $item ) || empty( $item[2] ) ) { continue; } if ( 'add-from-server-reloaded' === (string) $item[2] ) { $submenu['add-from-server-reloaded'][ $index ][0] = $top_label; } } // PRO badge only on History + Scheduled Imports (not every Pro submenu). $pro_slugs = array( 'add-from-server-reloaded-history' => true, 'add-from-server-reloaded-scheduler' => true, ); if ( $pro_active ) { // Pro: feature menus first; Settings after Access Control. $desired = array( 'add-from-server-reloaded', 'add-from-server-reloaded-history', 'add-from-server-reloaded-scheduler', 'add-from-server-reloaded-remote', 'add-from-server-reloaded-duplicates', 'add-from-server-reloaded-email', 'add-from-server-reloaded-access', 'add-from-server-reloaded-settings', ); } else { // Lite: Settings 2nd; Pro Features / Get Pro stay at the end. $desired = array( 'add-from-server-reloaded', 'add-from-server-reloaded-settings', 'add-from-server-reloaded-history', 'add-from-server-reloaded-scheduler', 'add-from-server-reloaded-remote', 'add-from-server-reloaded-duplicates', 'add-from-server-reloaded-email', 'add-from-server-reloaded-access', self::PAGE_FEATURES, ); } $by_slug = array(); $extras = array(); foreach ( $submenu['add-from-server-reloaded'] as $item ) { if ( ! is_array( $item ) || empty( $item[2] ) ) { continue; } $slug = (string) $item[2]; if ( ! $pro_active && isset( $pro_slugs[ $slug ] ) && false === strpos( (string) $item[0], 'afsr-menu-pro-badge' ) ) { $item[0] = wp_strip_all_tags( (string) $item[0] ) . ' PRO'; } if ( 0 === strpos( $slug, 'http://' ) || 0 === strpos( $slug, 'https://' ) ) { if ( false === strpos( (string) $item[0], 'afsr-menu-external' ) ) { $item[0] = wp_strip_all_tags( (string) $item[0] ) . ' '; } $extras[] = $item; continue; } $by_slug[ $slug ] = $item; } $ordered = array(); foreach ( $desired as $slug ) { if ( isset( $by_slug[ $slug ] ) ) { $ordered[] = $by_slug[ $slug ]; unset( $by_slug[ $slug ] ); } } foreach ( $by_slug as $item ) { $ordered[] = $item; } foreach ( $extras as $item ) { $ordered[] = $item; } $submenu['add-from-server-reloaded'] = $ordered; } /** * Catalog of Pro features (non-technical one-line descriptions). * * @since 5.4.2 * * @return array */ public static function feature_catalog() { return array( 'background' => array( 'title' => __( 'Keep importing after you leave', 'add-from-server-reloaded' ), 'description' => __( 'Start a large import, then leave the page. Pro keeps working in the background until it finishes.', 'add-from-server-reloaded' ), 'slug' => 'background', ), 'defer_thumbnails' => array( 'title' => __( 'Faster image imports', 'add-from-server-reloaded' ), 'description' => __( 'Import photos first, then create the smaller preview sizes later so big image jobs finish sooner.', 'add-from-server-reloaded' ), 'slug' => 'defer_thumbnails', ), 'folder_preserve' => array( 'title' => __( 'Keep your folder layout', 'add-from-server-reloaded' ), 'description' => __( 'Imported files keep the same folder / subfolder layout they had on the server, instead of only a date folder.', 'add-from-server-reloaded' ), 'slug' => 'folder_preserve', ), 'advanced_duplicates' => array( 'title' => __( 'Smarter duplicate handling', 'add-from-server-reloaded' ), 'description' => __( 'Choose to skip, replace, or keep both copies when the same file already exists, plus tools to clean up duplicates.', 'add-from-server-reloaded' ), 'slug' => 'advanced_duplicates', ), 'queue_controls' => array( 'title' => __( 'Pause, resume, and retry', 'add-from-server-reloaded' ), 'description' => __( 'Pause a long import, continue later, and retry only the files that failed, without starting over.', 'add-from-server-reloaded' ), 'slug' => 'queue_controls', ), 'history' => array( 'title' => __( 'Import History', 'add-from-server-reloaded' ), 'description' => __( 'See every past import job, what succeeded or failed, and open details for each file.', 'add-from-server-reloaded' ), 'slug' => 'history', ), 'scheduled_imports' => array( 'title' => __( 'Scheduled Imports', 'add-from-server-reloaded' ), 'description' => __( 'Automatically pull files from a folder on a schedule (hourly, daily, and more) without doing it by hand.', 'add-from-server-reloaded' ), 'slug' => 'scheduled_imports', ), 'ftp_sftp' => array( 'title' => __( 'Remote Sources (FTP / SFTP / cloud)', 'add-from-server-reloaded' ), 'description' => __( 'Connect to another server or S3-style cloud storage, pick files there, then import them into WordPress.', 'add-from-server-reloaded' ), 'slug' => 'ftp_sftp', ), 'email_notifications' => array( 'title' => __( 'Email alerts', 'add-from-server-reloaded' ), 'description' => __( 'Get an email when an import finishes or fails, so you do not have to watch the screen.', 'add-from-server-reloaded' ), 'slug' => 'email_notifications', ), 'rbac' => array( 'title' => __( 'Access Control', 'add-from-server-reloaded' ), 'description' => __( 'Choose which user roles can only browse files, and which roles are allowed to import.', 'add-from-server-reloaded' ), 'slug' => 'rbac', ), 'rest_api' => array( 'title' => __( 'REST API', 'add-from-server-reloaded' ), 'description' => __( 'Let other apps or automation tools start and check imports through WordPress’s API.', 'add-from-server-reloaded' ), 'slug' => 'rest_api', ), 'wp_cli' => array( 'title' => __( 'Command-line tools (WP-CLI)', 'add-from-server-reloaded' ), 'description' => __( 'Run imports and schedules from the server command line. Useful for developers and hosting scripts.', 'add-from-server-reloaded' ), 'slug' => 'wp_cli', ), ); } /** * Feature map for JavaScript (slug => title/description). * * @since 5.4.2 * * @return array */ public static function feature_catalog_for_js() { $out = array(); foreach ( self::feature_catalog() as $slug => $item ) { $out[ $slug ] = array( 'title' => $item['title'], 'description' => $item['description'], ); } return $out; } /** * Small PRO badge HTML. * * @since 5.4.2 * * @return string */ public static function badge_html() { return ''; } /** * Compact “Pro options available” summary for the import page. * * @since 5.4.2 */ public static function render_import_summary() { if ( self::is_pro_active() ) { return; } ?> __( 'Import History', 'add-from-server-reloaded' ), 'slug' => 'add-from-server-reloaded-history', 'callback' => array( Pro_Locked_Screens::class, 'history' ), 'cap' => 'upload_files', ), array( 'title' => __( 'Scheduled Imports', 'add-from-server-reloaded' ), 'slug' => 'add-from-server-reloaded-scheduler', 'callback' => array( Pro_Locked_Screens::class, 'schedules' ), 'cap' => 'manage_options', ), array( 'title' => __( 'Remote Sources', 'add-from-server-reloaded' ), 'slug' => 'add-from-server-reloaded-remote', 'callback' => array( Pro_Locked_Screens::class, 'remote' ), 'cap' => 'manage_options', ), array( 'title' => __( 'Duplicates', 'add-from-server-reloaded' ), 'slug' => 'add-from-server-reloaded-duplicates', 'callback' => array( Pro_Locked_Screens::class, 'duplicates' ), 'cap' => 'upload_files', ), array( 'title' => __( 'Email Alerts', 'add-from-server-reloaded' ), 'slug' => 'add-from-server-reloaded-email', 'callback' => array( Pro_Locked_Screens::class, 'email' ), 'cap' => 'manage_options', ), array( 'title' => __( 'Access Control', 'add-from-server-reloaded' ), 'slug' => 'add-from-server-reloaded-access', 'callback' => array( Pro_Locked_Screens::class, 'access' ), 'cap' => 'manage_options', ), ); foreach ( $locked as $item ) { if ( ! current_user_can( $item['cap'] ) ) { continue; } $hook = add_submenu_page( 'add-from-server-reloaded', $item['title'], $item['title'], $item['cap'], $item['slug'], $item['callback'] ); if ( $hook ) { add_action( 'load-' . $hook, static function () { self::enqueue_locked_ui_assets(); } ); } } $hook = add_submenu_page( 'add-from-server-reloaded', __( 'Pro Features', 'add-from-server-reloaded' ), __( 'Pro Features', 'add-from-server-reloaded' ), 'upload_files', self::PAGE_FEATURES, array( __CLASS__, 'render_features_page' ) ); if ( $hook ) { add_action( 'load-' . $hook, static function () { self::enqueue_locked_ui_assets(); } ); } add_action( 'admin_menu', array( __CLASS__, 'append_get_pro_menu_link' ), 999 ); } /** * Append external Get Pro link as the last submenu item only. * * WordPress uses the first submenu slug as the parent menu URL. An external * URL must never sit at index 0 or "Add From Server" will leave wp-admin. * * @since 5.4.2 */ public static function append_get_pro_menu_link() { if ( self::is_pro_active() ) { return; } global $submenu; if ( empty( $submenu['add-from-server-reloaded'] ) || ! is_array( $submenu['add-from-server-reloaded'] ) ) { return; } $upgrade = self::upgrade_url(); // Drop any prior Get Pro entries (avoid duplicates on re-register). $submenu['add-from-server-reloaded'] = array_values( array_filter( $submenu['add-from-server-reloaded'], static function ( $item ) use ( $upgrade ) { return ! ( is_array( $item ) && isset( $item[2] ) && (string) $item[2] === $upgrade ); } ) ); // If index 0 was somehow an external URL, restore the main plugin page first. $first_slug = isset( $submenu['add-from-server-reloaded'][0][2] ) ? (string) $submenu['add-from-server-reloaded'][0][2] : ''; if ( 0 === strpos( $first_slug, 'http://' ) || 0 === strpos( $first_slug, 'https://' ) ) { array_unshift( $submenu['add-from-server-reloaded'], array( Features::is_pro() ? __( 'AFS Pro', 'add-from-server-reloaded' ) : __( 'AFS Lite', 'add-from-server-reloaded' ), 'upload_files', 'add-from-server-reloaded', ) ); } $submenu['add-from-server-reloaded'][] = array( __( 'Get Pro', 'add-from-server-reloaded' ), 'upload_files', $upgrade, ); } /** * Full Pro Features list page. * * @since 5.4.2 */ public static function render_features_page() { if ( self::is_pro_active() ) { wp_safe_redirect( admin_url( 'admin.php?page=add-from-server-reloaded' ) ); exit; } self::enqueue_locked_ui_assets(); $cards = self::features_page_cards(); $upgrade = self::upgrade_url(); ?>

*/ public static function features_page_cards() { return array( array( 'title' => __( 'Keep importing after you leave', 'add-from-server-reloaded' ), 'description' => __( 'Start a large import, then leave the page. Pro keeps working until it finishes.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Faster image imports', 'add-from-server-reloaded' ), 'description' => __( 'Import files first, generate thumbnail sizes later so big batches finish sooner.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Keep your folder layout', 'add-from-server-reloaded' ), 'description' => __( 'Imported files keep the same folder structure they had on the server.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Smarter duplicate handling', 'add-from-server-reloaded' ), 'description' => __( 'Skip, replace, or import as new when a file already exists.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Pause, resume, and retry', 'add-from-server-reloaded' ), 'description' => __( 'Pause a long import and continue later. Retry only the files that failed.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Import History', 'add-from-server-reloaded' ), 'description' => __( 'See every past import job, what succeeded or failed, and open per-file details.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Scheduled Imports', 'add-from-server-reloaded' ), 'description' => __( 'Pull files from a folder automatically, on a schedule, no manual runs needed.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Remote Sources', 'add-from-server-reloaded' ), 'description' => __( 'Connect FTP, SFTP, or S3-compatible storage, browse it, and import straight in.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Email alerts', 'add-from-server-reloaded' ), 'description' => __( 'Get an email when an import finishes or fails, so you don\'t have to watch it.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'Access Control', 'add-from-server-reloaded' ), 'description' => __( 'Choose which roles can browse files and which can run imports.', 'add-from-server-reloaded' ), ), array( 'title' => __( 'REST API & WP-CLI', 'add-from-server-reloaded' ), 'description' => __( 'Start and check imports from other tools, automations, or the command line.', 'add-from-server-reloaded' ), ), ); } /** * Enqueue teaser assets on our admin screens. * * @since 5.4.2 * * @param string $hook Current admin hook. */ public static function enqueue_assets( $hook ) { unset( $hook ); if ( ! self::is_plugin_screen() ) { return; } wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'add-from-server-reloaded' ); wp_enqueue_script( 'add-from-server-reloaded' ); wp_register_style( 'afsrreloaded-pro-teaser', plugins_url( 'admin/css/pro-teaser.css', AFSRRELOADED_PLUGIN_FILE ), array( 'add-from-server-reloaded' ), AFSRRELOADED_VERSION ); wp_register_script( 'afsrreloaded-pro-teaser', plugins_url( 'admin/js/pro-teaser.js', AFSRRELOADED_PLUGIN_FILE ), array( 'jquery', 'add-from-server-reloaded' ), AFSRRELOADED_VERSION, true ); wp_localize_script( 'afsrreloaded-pro-teaser', 'afsrreloadedProTeaser', array( 'isPro' => self::is_pro_active(), 'upgradeUrl' => self::upgrade_url(), 'features' => self::is_pro_active() ? array() : self::feature_catalog_for_js(), 'i18n' => array( 'defaultTitle' => __( 'This is a Pro feature', 'add-from-server-reloaded' ), 'upgrade' => __( 'Upgrade to Pro', 'add-from-server-reloaded' ), ), ) ); wp_enqueue_style( 'afsrreloaded-pro-teaser' ); wp_enqueue_script( 'afsrreloaded-pro-teaser' ); // Force brand primary button color on all plugin screens (Chrome/Firefox parity). $brand = self::BRAND_PRIMARY; $hover = '#1430b8'; $css = ' body.toplevel_page_add-from-server-reloaded .wrap .button-primary, body[class*="add-from-server-reloaded"] .wrap .button-primary, body[class*="add-from-server-reloaded"] .afsrreloaded-pro-modal .button-primary { background: ' . $brand . ' !important; border-color: ' . $brand . ' !important; color: #fff !important; text-shadow: none !important; box-shadow: none !important; } body.toplevel_page_add-from-server-reloaded .wrap .button-primary:hover, body.toplevel_page_add-from-server-reloaded .wrap .button-primary:focus, body[class*="add-from-server-reloaded"] .wrap .button-primary:hover, body[class*="add-from-server-reloaded"] .wrap .button-primary:focus, body[class*="add-from-server-reloaded"] .afsrreloaded-pro-modal .button-primary:hover, body[class*="add-from-server-reloaded"] .afsrreloaded-pro-modal .button-primary:focus { background: ' . $hover . ' !important; border-color: ' . $hover . ' !important; color: #fff !important; } .afsrreloaded-pro-badge { background: ' . $brand . ' !important; } '; wp_add_inline_style( 'afsrreloaded-pro-teaser', $css ); } /** * Whether the current admin screen belongs to this plugin. * * @since 5.4.2 * * @return bool */ protected static function is_plugin_screen() { if ( ! is_admin() ) { return false; } $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( '' === $page ) { return false; } return 0 === strpos( $page, 'add-from-server-reloaded' ); } }