From 5b3374e2d51f5ecf5ddcff157565b897ac42cab1 Mon Sep 17 00:00:00 2001 From: myprestarocks Date: Sat, 14 Feb 2026 09:33:48 +0000 Subject: [PATCH] Add Smarty plugin, auto-detect theme icons, PS 1.6 compat, new icons - Add {mpr_icon name='x'} Smarty plugin for .tpl templates - Auto-detect theme icon set (FA vs Material) without config - Self-host icon fonts automatically if theme doesn't include them - PS 1.6 compatible (addCSS fallback for registerStylesheet) - Add 16 new icons: camera, play, video, chart, pie-chart, trending-up/down, tune, build, swap, touch, inventory, payments, campaign, trophy, schedule, history, verified, bolt - Simplify MprIconsAssets: automatic inclusion, no config toggle needed - Multiple modules calling registerAssets = single CSS load (dedup) Co-Authored-By: Claude Opus 4.6 --- src/MprIcons.php | 209 +++++++++++++++++++++++++++++++++-------- src/MprIconsAssets.php | 132 +++++++++----------------- src/MprIconsConfig.php | 118 ++--------------------- 3 files changed, 219 insertions(+), 240 deletions(-) diff --git a/src/MprIcons.php b/src/MprIcons.php index 2395b91..97d3724 100644 --- a/src/MprIcons.php +++ b/src/MprIcons.php @@ -5,8 +5,13 @@ * * Simple API: MprIcons::get('account') - that's it. * - * - Admin: auto-detects and uses Material Icons - * - Front: reads icon set from ps_mpr_config table + * - Auto-detects theme icon set (Font Awesome or Material Icons) + * - Admin: always Material Icons + * - Front: detects from theme, falls back to Font Awesome + * - Self-hosts icon fonts if theme doesn't include them + * - Smarty: {mpr_icon name='account'} in .tpl templates + * + * Compatible with PrestaShop 1.6 through 9.1 * * @package myprestarocks/prestashop-icons */ @@ -23,6 +28,7 @@ class MprIcons const CONFIG_KEY = 'front_icon_set'; private static $iconSet = null; + private static $smartyRegistered = false; /** * Semantic name => [FA class, Material name] @@ -42,6 +48,10 @@ class MprIcons 'gift' => ['fa fa-gift', 'card_giftcard'], 'tag' => ['fa fa-tag', 'local_offer'], 'percent' => ['fa fa-percent', 'percent'], + 'inventory' => ['fa fa-cube', 'inventory_2'], + 'payments' => ['fa fa-money', 'payments'], + 'campaign' => ['fa fa-bullhorn', 'campaign'], + 'trophy' => ['fa fa-trophy', 'emoji_events'], // Status 'success' => ['fa fa-check', 'check'], @@ -60,6 +70,16 @@ class MprIcons 'save' => ['fa fa-save', 'save'], 'print' => ['fa fa-print', 'print'], 'copy' => ['fa fa-copy', 'content_copy'], + 'tune' => ['fa fa-sliders', 'tune'], + 'build' => ['fa fa-wrench', 'build'], + 'swap' => ['fa fa-exchange', 'swap_horiz'], + 'touch' => ['fa fa-hand-pointer-o', 'touch_app'], + + // Analytics & Charts + 'chart' => ['fa fa-bar-chart', 'bar_chart'], + 'pie-chart' => ['fa fa-pie-chart', 'pie_chart'], + 'trending-up' => ['fa fa-line-chart', 'trending_up'], + 'trending-down' => ['fa fa-line-chart', 'trending_down'], // UI 'close' => ['fa fa-times', 'close'], @@ -90,6 +110,18 @@ class MprIcons 'unlock' => ['fa fa-unlock', 'lock_open'], 'shield' => ['fa fa-shield', 'shield'], 'key' => ['fa fa-key', 'vpn_key'], + 'verified' => ['fa fa-check-circle', 'verified_user'], + 'bolt' => ['fa fa-bolt', 'bolt'], + + // Media + 'camera' => ['fa fa-camera', 'add_a_photo'], + 'play' => ['fa fa-play-circle', 'play_circle'], + 'image' => ['fa fa-image', 'image'], + 'video' => ['fa fa-video-camera', 'videocam'], + + // Time & History + 'schedule' => ['fa fa-clock-o', 'schedule'], + 'history' => ['fa fa-history', 'history'], // Misc 'home' => ['fa fa-home', 'home'], @@ -102,7 +134,6 @@ class MprIcons 'eye-off' => ['fa fa-eye-slash', 'visibility_off'], 'file' => ['fa fa-file', 'description'], 'folder' => ['fa fa-folder', 'folder'], - 'image' => ['fa fa-image', 'image'], 'link' => ['fa fa-link', 'link'], 'external' => ['fa fa-external-link', 'open_in_new'], 'help' => ['fa fa-question-circle', 'help'], @@ -151,6 +182,34 @@ class MprIcons return $set === self::MATERIAL ? $material : $fa; } + /** + * Smarty {mpr_icon} function handler + * + * Usage in .tpl: + * {mpr_icon name='account'} + * {mpr_icon name='cart' class='text-primary'} + */ + public static function smartyFunction($params, $smarty): string + { + return self::get($params['name'] ?? '', $params['class'] ?? ''); + } + + /** + * Register {mpr_icon} as a Smarty function plugin + * Safe to call multiple times — registers only once + * + * @param \Smarty|\SmartyBC $smarty Smarty instance + */ + public static function registerSmartyPlugin($smarty): void + { + if (self::$smartyRegistered) { + return; + } + + $smarty->registerPlugin('function', 'mpr_icon', [self::class, 'smartyFunction']); + self::$smartyRegistered = true; + } + /** * Detect and cache the icon set to use */ @@ -160,14 +219,21 @@ class MprIcons return self::$iconSet; } - // Admin context = Material Icons + // Admin context = Material Icons (PS 1.7+ admin always loads them) if (defined('_PS_ADMIN_DIR_')) { self::$iconSet = self::MATERIAL; return self::$iconSet; } - // Front office - read from config table - self::$iconSet = self::readConfigTable() ?: self::FA; + // Front office - check config table first, then auto-detect + $fromConfig = self::readConfigTable(); + if ($fromConfig !== null) { + self::$iconSet = $fromConfig; + return self::$iconSet; + } + + // No config stored — auto-detect from theme + self::$iconSet = self::detectThemeIcons(); return self::$iconSet; } @@ -192,7 +258,7 @@ class MprIcons return $result; } } catch (\Exception $e) { - // Table doesn't exist or query failed - use default + // Table doesn't exist or query failed } return null; @@ -222,7 +288,7 @@ class MprIcons $result = \Db::getInstance()->execute($sql); if ($result) { - self::$iconSet = null; // Clear cache + self::$iconSet = null; } return $result; @@ -250,8 +316,8 @@ class MprIcons } /** - * Initialize icon system - call from module install - * Creates config table if needed, sets default icon set + * Initialize icon system — call from module install + * Creates config table if needed, detects and saves icon set */ public static function init(): bool { @@ -259,7 +325,6 @@ class MprIcons return false; } - // If no config exists, detect and save default $current = self::readConfigTable(); if ($current === null) { $detected = self::detectThemeIcons(); @@ -281,7 +346,6 @@ class MprIcons try { $table = _DB_PREFIX_ . self::CONFIG_TABLE; - // Check if table exists $exists = \Db::getInstance()->getValue( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() @@ -292,7 +356,8 @@ class MprIcons return true; } - // Create table + $engine = defined('_MYSQL_ENGINE_') ? _MYSQL_ENGINE_ : 'InnoDB'; + $sql = "CREATE TABLE IF NOT EXISTS `{$table}` ( `id_config` INT UNSIGNED NOT NULL AUTO_INCREMENT, `module` VARCHAR(64) NOT NULL, @@ -303,7 +368,7 @@ class MprIcons PRIMARY KEY (`id_config`), UNIQUE KEY `module_key` (`module`, `key`), KEY `module` (`module`) - ) ENGINE=" . _MYSQL_ENGINE_ . " DEFAULT CHARSET=utf8mb4;"; + ) ENGINE={$engine} DEFAULT CHARSET=utf8mb4;"; return \Db::getInstance()->execute($sql); } catch (\Exception $e) { @@ -312,46 +377,121 @@ class MprIcons } /** - * Detect which icon set the current theme likely uses - * Returns 'fontawesome' for most PrestaShop themes + * Detect which icon set the current theme uses + * + * Checks theme config and assets for Material Icons indicators. + * Returns 'fontawesome' for classic and most themes, + * 'material' for Hummingbird and Material-based themes. */ public static function detectThemeIcons(): string { - // Most PrestaShop themes (including classic) use Font Awesome - // Only return Material if we can positively detect it - if (!defined('_PS_THEME_DIR_')) { return self::FA; } try { - // Check theme.yml for icon hints + // PS 1.7+ — check theme.yml $themeYml = _PS_THEME_DIR_ . 'config/theme.yml'; if (file_exists($themeYml)) { $content = file_get_contents($themeYml); - if (strpos($content, 'material') !== false || strpos($content, 'Material') !== false) { + if (stripos($content, 'hummingbird') !== false) { + return self::MATERIAL; + } + if (stripos($content, 'material-icons') !== false || stripos($content, 'material_icons') !== false) { return self::MATERIAL; } } - // Check if theme has material-icons in assets - $assetsDir = _PS_THEME_DIR_ . 'assets/css/'; - if (is_dir($assetsDir)) { - $files = scandir($assetsDir); + // Check theme assets for material-icons CSS + $dirs = [ + _PS_THEME_DIR_ . 'assets/css/', + _PS_THEME_DIR_ . 'css/', + ]; + foreach ($dirs as $dir) { + if (!is_dir($dir)) { + continue; + } + $files = scandir($dir); foreach ($files as $file) { - if (strpos($file, 'material') !== false) { + if (stripos($file, 'material') !== false) { return self::MATERIAL; } } } + + // PS 1.6 — check for parent theme name + if (defined('_THEME_NAME_')) { + $themeName = _THEME_NAME_; + if (stripos($themeName, 'hummingbird') !== false) { + return self::MATERIAL; + } + } } catch (\Exception $e) { - // Ignore detection errors + // Detection failed — use safe default } - // Default to Font Awesome (safe for 95% of themes) return self::FA; } + /** + * Check if the current theme already includes the needed icon font + * Used by MprIconsAssets to decide whether to self-host + */ + public static function themeHasIconFont(): bool + { + if (!defined('_PS_THEME_DIR_')) { + return false; + } + + $iconSet = self::getIconSet(); + + try { + $dirs = [ + _PS_THEME_DIR_ . 'assets/css/', + _PS_THEME_DIR_ . 'css/', + ]; + + foreach ($dirs as $dir) { + if (!is_dir($dir)) { + continue; + } + + $files = scandir($dir); + foreach ($files as $file) { + if ($file === '.' || $file === '..') { + continue; + } + + if ($iconSet === self::MATERIAL) { + if (stripos($file, 'material') !== false) { + return true; + } + } else { + if (stripos($file, 'fontawesome') !== false || stripos($file, 'font-awesome') !== false) { + return true; + } + } + } + } + + // For Font Awesome — most PS 1.6/1.7 themes include it as a dependency + // Check if theme's package.json or theme.yml lists it + if ($iconSet === self::FA) { + $themeYml = _PS_THEME_DIR_ . 'config/theme.yml'; + if (file_exists($themeYml)) { + $content = file_get_contents($themeYml); + if (stripos($content, 'font-awesome') !== false || stripos($content, 'fontawesome') !== false) { + return true; + } + } + } + } catch (\Exception $e) { + // Detection failed + } + + return false; + } + /** * Check if icon exists */ @@ -369,9 +509,8 @@ class MprIcons } /** - * Register icon font CSS assets - * Convenience method that delegates to MprIconsAssets - * Call from hookHeader or setMedia + * Register icon font CSS assets if theme doesn't provide them + * Delegates to MprIconsAssets. Safe to call from multiple modules. * * @param \Module $module The module instance * @return bool Whether assets were registered @@ -380,12 +519,4 @@ class MprIcons { return MprIconsAssets::registerAssets($module); } - - /** - * Check if self-hosted icon fonts are enabled - */ - public static function isSelfHostEnabled(): bool - { - return MprIconsConfig::isSelfHostEnabled(); - } } diff --git a/src/MprIconsAssets.php b/src/MprIconsAssets.php index 198499d..c30adba 100644 --- a/src/MprIconsAssets.php +++ b/src/MprIconsAssets.php @@ -1,11 +1,13 @@ getLocalPath(); $assetsPath = self::getAssetsPath(); - // Get relative path from module to assets $relativePath = str_replace($modulePath, '', $assetsPath); return $module->getPathUri() . ltrim($relativePath, '/'); } /** - * Register icon font CSS if self-hosting is enabled - * Call this from your module's hookHeader or setMedia + * Register icon font CSS — automatic detection + * + * 1. Skips in admin context (admin always has Material Icons) + * 2. Checks if theme already provides the needed icon font + * 3. If not, registers bundled CSS from this package + * 4. Uses fixed registration ID — multiple modules calling = one load + * + * Call from hookActionFrontControllerSetMedia or hookHeader. * * @param \Module $module The module instance - * @return bool Whether assets were registered + * @return bool Whether assets were registered (false if skipped or already loaded) */ public static function registerAssets(\Module $module): bool { - // Skip in admin context (admin always has Material Icons) + // Skip in admin context if (defined('_PS_ADMIN_DIR_')) { return false; } - // Check if self-hosting is enabled - if (!MprIconsConfig::isSelfHostEnabled()) { + // Already registered by another module in this request + if (self::$registered) { + return false; + } + + // Theme already includes the needed icon font — skip + if (MprIcons::themeHasIconFont()) { + self::$registered = true; return false; } @@ -70,14 +82,23 @@ class MprIconsAssets $cssFile = $assetsUrl . '/fontawesome/fontawesome.min.css'; } - // Register CSS $controller = \Context::getContext()->controller; - if ($controller instanceof \FrontController) { + + // PS 1.7+ — use registerStylesheet (deduplicates by ID) + if (method_exists($controller, 'registerStylesheet')) { $controller->registerStylesheet( 'mpr-icons-' . $iconSet, $cssFile, - ['media' => 'all', 'priority' => 50] + ['media' => 'all', 'priority' => 50, 'server' => 'remote'] ); + self::$registered = true; + return true; + } + + // PS 1.6 — use addCSS (deduplicates by URI) + if (method_exists($controller, 'addCSS')) { + $controller->addCSS($cssFile, 'all'); + self::$registered = true; return true; } @@ -85,17 +106,14 @@ class MprIconsAssets } /** - * Get CSS link tag HTML (for manual inclusion) + * Get CSS link tag HTML for manual inclusion + * Useful for email templates or non-standard contexts * * @param \Module $module The module instance - * @return string HTML link tag or empty string + * @return string HTML link tag */ public static function getCssTag(\Module $module): string { - if (!MprIconsConfig::isSelfHostEnabled()) { - return ''; - } - $iconSet = MprIcons::getIconSet(); $assetsUrl = self::getAssetsUrl($module); @@ -111,7 +129,7 @@ class MprIconsAssets /** * Check if bundled assets exist * - * @param string $iconSet The icon set to check ('fontawesome' or 'material') + * @param string $iconSet The icon set to check * @return bool */ public static function assetsExist(string $iconSet): bool @@ -124,70 +142,4 @@ class MprIconsAssets return file_exists($assetsPath . '/fontawesome/fontawesome.min.css'); } - - /** - * Copy assets to module's public directory - * Use this if your module needs assets in a specific location - * - * @param string $targetDir Target directory - * @param string $iconSet Which icon set to copy - * @return bool Success - */ - public static function copyAssets(string $targetDir, string $iconSet): bool - { - $assetsPath = self::getAssetsPath(); - - if ($iconSet === MprIcons::MATERIAL) { - $sourceDir = $assetsPath . '/material-icons'; - } else { - $sourceDir = $assetsPath . '/fontawesome'; - } - - if (!is_dir($sourceDir)) { - return false; - } - - return self::recursiveCopy($sourceDir, $targetDir); - } - - /** - * Recursively copy directory - */ - private static function recursiveCopy(string $source, string $dest): bool - { - if (!is_dir($dest)) { - if (!mkdir($dest, 0755, true)) { - return false; - } - } - - $dir = opendir($source); - if (!$dir) { - return false; - } - - while (($file = readdir($dir)) !== false) { - if ($file === '.' || $file === '..') { - continue; - } - - $srcPath = $source . '/' . $file; - $destPath = $dest . '/' . $file; - - if (is_dir($srcPath)) { - if (!self::recursiveCopy($srcPath, $destPath)) { - closedir($dir); - return false; - } - } else { - if (!copy($srcPath, $destPath)) { - closedir($dir); - return false; - } - } - } - - closedir($dir); - return true; - } } diff --git a/src/MprIconsConfig.php b/src/MprIconsConfig.php index b41a3e5..038f63f 100644 --- a/src/MprIconsConfig.php +++ b/src/MprIconsConfig.php @@ -1,11 +1,11 @@ 'select', 'label' => 'Front Office Icon Set', 'name' => $formName . '_ICON_SET', - 'desc' => 'Choose which icon font to use on the front office. Most themes include Font Awesome.', + 'desc' => 'Choose which icon font to use on the front office. Auto-detected from theme on install. Override here if needed.', 'options' => [ 'query' => [ - ['id' => MprIcons::FA, 'name' => 'Font Awesome (default)'], - ['id' => MprIcons::MATERIAL, 'name' => 'Material Icons'], + ['id' => MprIcons::FA, 'name' => 'Font Awesome (classic theme)'], + ['id' => MprIcons::MATERIAL, 'name' => 'Material Icons (hummingbird theme)'], ], 'id' => 'id', 'name' => 'name', ], ], - [ - 'type' => 'switch', - 'label' => 'Include Icon Fonts from Module', - 'name' => $formName . '_SELFHOST', - 'desc' => 'Enable this if your theme does not load icon fonts. The module will include the necessary CSS.', - 'is_bool' => true, - 'values' => [ - ['id' => 'active_on', 'value' => 1, 'label' => 'Yes'], - ['id' => 'active_off', 'value' => 0, 'label' => 'No'], - ], - ], ]; } @@ -64,7 +51,6 @@ class MprIconsConfig { return [ $formName . '_ICON_SET' => MprIcons::getIconSet(), - $formName . '_SELFHOST' => self::isSelfHostEnabled() ? 1 : 0, ]; } @@ -77,96 +63,6 @@ class MprIconsConfig public static function processForm(string $formName = 'MPR_ICONS'): bool { $iconSet = \Tools::getValue($formName . '_ICON_SET', MprIcons::FA); - $selfHost = (int) \Tools::getValue($formName . '_SELFHOST', 0); - - $success = MprIcons::saveIconSet($iconSet); - $success = self::saveSelfHostOption($selfHost) && $success; - - return $success; - } - - /** - * Check if self-hosting is enabled - */ - public static function isSelfHostEnabled(): bool - { - $value = self::readConfig(self::CONFIG_KEY_SELFHOST); - return $value === '1'; - } - - /** - * Save self-host option - */ - public static function saveSelfHostOption(int $enabled): bool - { - return self::saveConfig(self::CONFIG_KEY_SELFHOST, $enabled ? '1' : '0'); - } - - /** - * Read config from mpr_config table - */ - private static function readConfig(string $key): ?string - { - if (!defined('_DB_PREFIX_')) { - return null; - } - - try { - $table = _DB_PREFIX_ . MprIcons::CONFIG_TABLE; - $sql = "SELECT `value` FROM `{$table}` - WHERE `module` = '" . pSQL(MprIcons::CONFIG_MODULE) . "' - AND `key` = '" . pSQL($key) . "'"; - - $result = \Db::getInstance()->getValue($sql); - return $result !== false ? $result : null; - } catch (\Exception $e) { - return null; - } - } - - /** - * Save config to mpr_config table - */ - private static function saveConfig(string $key, string $value): bool - { - if (!defined('_DB_PREFIX_')) { - return false; - } - - // Ensure table exists - MprIcons::ensureConfigTable(); - - try { - $table = _DB_PREFIX_ . MprIcons::CONFIG_TABLE; - $now = date('Y-m-d H:i:s'); - - $sql = "INSERT INTO `{$table}` (`module`, `key`, `value`, `date_add`, `date_upd`) - VALUES ('" . pSQL(MprIcons::CONFIG_MODULE) . "', '" . pSQL($key) . "', '" . pSQL($value) . "', '{$now}', '{$now}') - ON DUPLICATE KEY UPDATE `value` = '" . pSQL($value) . "', `date_upd` = '{$now}'"; - - return \Db::getInstance()->execute($sql); - } catch (\Exception $e) { - return false; - } - } - - /** - * Get complete form input array for a HelperForm - * Includes form wrapper with legend - * - * @param string $formName Form name prefix - * @return array Complete form input structure - */ - public static function getFormInput(string $formName = 'MPR_ICONS'): array - { - return [ - 'form' => [ - 'legend' => [ - 'title' => 'Icon Settings', - 'icon' => 'icon-picture', - ], - 'input' => self::getFormFields($formName), - ], - ]; + return MprIcons::saveIconSet($iconSet); } }