| | |
| | | protected array $customIcons = []; |
| | | protected array $usedIcons = []; |
| | | protected array $map = []; |
| | | protected const MAX_VERSIONS = 5; |
| | | |
| | | /** |
| | | * Get singleton instance |
| | |
| | | private function __construct() |
| | | { |
| | | $this->cache = CacheManager::for('icons', WEEK_IN_SECONDS); |
| | | |
| | | $this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles)) |
| | | ? JVB_SITE['icons'] |
| | | : 'regular'; |
| | |
| | | ]); |
| | | |
| | | |
| | | $this->usedIcons = get_option(BASE.'used_icons', []); |
| | | $this->usedIcons = get_option(BASE.'usedIcons', []); |
| | | $this->includeIcons(); |
| | | // Track custom icons for CSS generation |
| | | $this->trackCustomIcons(); |
| | | // Register hooks only once |
| | | $this->registerHooks(); |
| | | } |
| | | |
| | | /** |
| | | * Ensure custom icons are tracked for CSS generation |
| | | */ |
| | | protected function trackCustomIcons(): void |
| | | { |
| | | if (empty($this->customIcons)) { |
| | | return; |
| | | } |
| | | |
| | | foreach ($this->customIcons as $name => $path) { |
| | | $this->trackIconUsage($name, $this->style); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Include icons via filter (for JS usage, etc.) |
| | | */ |
| | | protected function includeIcons():array |
| | | protected function includeIcons():void |
| | | { |
| | | $icons = get_option(BASE.'includeIcons'); |
| | | // $icons = false; |
| | | if (!$icons) { |
| | | $icons = $this->addIncludeIcons(); |
| | | } |
| | | $include = apply_filters('jvbIncludeIcons', []); |
| | | $add = array_filter($include, function($addIt) use ($icons) { |
| | | return !in_array($addIt, $icons); |
| | | }); |
| | | error_log('Adding icons: '.print_r($add, true)); |
| | | if (!empty($add)) { |
| | | $this->addIncludeIcons($add); |
| | | } |
| | | |
| | | return $icons; |
| | | } |
| | | |
| | | protected function addIncludeIcons(array $add = []) { |
| | | $icons = get_option(BASE.'includeIcons'); |
| | | if (!$icons) { |
| | | $icons = [ |
| | | 'check-circle', |
| | |
| | | 'file-txt', |
| | | 'file-xls', |
| | | ]; |
| | | |
| | | $check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER]; |
| | | foreach ($check as $constant) { |
| | | foreach ($constant as $key => $value) { |
| | |
| | | } |
| | | } |
| | | $icons = apply_filters('jvbIncludeIcons', $icons); |
| | | |
| | | $icons = $this->maybePrefixIcons($icons); |
| | | update_option(BASE.'includeIcons', $icons); |
| | | update_option(BASE.'icons_needs_update', true); |
| | | } |
| | | $add = apply_filters('jvbIncludeIcons', $add); |
| | | $add = array_filter($add, function($addIt) use ($icons) { |
| | | return !in_array($addIt, $icons); |
| | | }); |
| | | if (!empty($add)) { |
| | | $icons = array_merge($add, $icons); |
| | | update_option(BASE.'usedIcons', $icons); |
| | | update_option(BASE.'icons_needs_update', true); |
| | | |
| | | // Ensure icons are in the correct format (handle legacy data) |
| | | if (!$this->isIconsArrayPrefixed($icons)) { |
| | | $icons = $this->maybePrefixIcons($icons); |
| | | update_option(BASE.'includeIcons', $icons); |
| | | } |
| | | return $icons; |
| | | |
| | | $additional = apply_filters('jvbIncludeIcons', []); |
| | | if (!empty($additional)) { |
| | | $additional = $this->maybePrefixIcons($additional); |
| | | $merged = $this->mergeUsedIcons($icons, $additional); |
| | | |
| | | if ($icons != $merged) { |
| | | update_option(BASE.'includeIcons', $merged); |
| | | $icons = $merged; |
| | | } |
| | | } |
| | | |
| | | foreach ($icons as $style => $theIcons) { |
| | | foreach($theIcons as $icon) { |
| | | $this->trackIconUsage($icon, $style); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check if icons array is in the prefixed format [style => [icons]] |
| | | */ |
| | | protected function isIconsArrayPrefixed(array $icons): bool |
| | | { |
| | | if (empty($icons)) { |
| | | return true; |
| | | } |
| | | |
| | | // Check if first key is a valid style name |
| | | $first_key = array_key_first($icons); |
| | | if (!in_array($first_key, $this->styles)) { |
| | | return false; |
| | | } |
| | | |
| | | // Check if first value is an array |
| | | return is_array($icons[$first_key]); |
| | | } |
| | | |
| | | protected function maybePrefixIcons(array $icons):array |
| | | { |
| | | $out = []; |
| | | foreach ($icons as $icon) { |
| | | if (is_array($icon) && array_key_exists('style', $icon)) { |
| | | if (!array_key_exists($icon['style'], $out)) { |
| | | $out[$icon['style']] = []; |
| | | } |
| | | if (!in_array($icon['name'], $out[$icon['style']])) { |
| | | $out[$icon['style']][] = $icon['name']; |
| | | } |
| | | } elseif(is_array($icon)) { |
| | | $icon = $icon['name']; |
| | | } |
| | | if (!is_array($icon)) { |
| | | if (!array_key_exists($this->style, $out)) { |
| | | $out[$this->style] = []; |
| | | } |
| | | if (!in_array($icon, $out[$this->style])){ |
| | | $out[$this->style][] = $icon; |
| | | } |
| | | } |
| | | } |
| | | return $out; |
| | | } |
| | | |
| | | protected function addMap():void |
| | |
| | | */ |
| | | protected function registerHooks(): void |
| | | { |
| | | add_action('init', [$this, 'checkCSS']); |
| | | add_action('init', [$this, 'includeIcons'], 1); |
| | | add_action('init', [$this, 'checkCSS'], 10); |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | } |
| | |
| | | { |
| | | error_log('[IconsManager]:regenerateCSS'); |
| | | $css = $this->generateIconCSS(); |
| | | $css_path = JVB_DIR.'/assets/css/icons.css'; |
| | | $css_path = JVB_CHILD_DIR.'/assets/css/'; |
| | | if (!file_exists($css_path)) { |
| | | wp_mkdir_p($css_path); |
| | | } |
| | | $css_path .= '/icons.css'; |
| | | |
| | | |
| | | // Archive current version before overwriting |
| | | $this->archiveCurrentVersion($css); |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | CacheManager::updateTimestamp('icons'); |
| | |
| | | */ |
| | | public function getIcon(string $name, array $options = []): string |
| | | { |
| | | if (!array_key_exists('style', $options)) { |
| | | $options['style'] = $this->style; |
| | | } |
| | | $style = array_key_exists('style', $options) ? $options['style'] :$this->style; |
| | | $name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name; |
| | | |
| | | // Validate icon exists |
| | | if (!$this->iconExists($name, $options['style'])) { |
| | | if (!$this->iconExists($name, $style)) { |
| | | error_log('[IconsManager] Icon not found: ' . $name); |
| | | return ''; |
| | | } |
| | | |
| | | $style = $options['style'] ?? $this->style; |
| | | |
| | | |
| | | // Track icon usage |
| | | $this->trackIconUsage($name, $style); |
| | | |
| | | $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : ''; |
| | | // Build classes |
| | | $classes = ['icon', 'icon-' . $name]; |
| | | $classes = ['icon', 'icon-' . $name.$styleClass]; |
| | | if (!empty($options['class'])) { |
| | | $classes[] = $options['class']; |
| | | } |
| | |
| | | */ |
| | | protected function trackIconUsage(string $name, string $style): void |
| | | { |
| | | $needsUpdate = false; |
| | | |
| | | if (!array_key_exists($style, $this->usedIcons)) { |
| | | $this->usedIcons[$style] = []; |
| | | $needsUpdate = true; |
| | | } |
| | | |
| | | if (!in_array($name, $this->usedIcons[$style])) { |
| | | $this->usedIcons[$style][] = $name; |
| | | update_option(BASE.'used_icons', $this->usedIcons); |
| | | $needsUpdate = true; |
| | | } |
| | | |
| | | if ($needsUpdate) { |
| | | // Merge with existing option to never lose icons |
| | | $existing = get_option(BASE.'usedIcons', []); |
| | | $merged = $this->mergeUsedIcons($existing, $this->usedIcons); |
| | | update_option(BASE.'usedIcons', $merged); |
| | | |
| | | // Flag for regeneration on next init |
| | | update_option(BASE.'icons_needs_update', true); |
| | | |
| | |
| | | |
| | | wp_enqueue_style( |
| | | 'jvb-icons', |
| | | JVB_URL.'assets/css/icons.css', |
| | | JVB_CHILD_URL.'assets/css/icons.css', |
| | | [], |
| | | $timestamp |
| | | ); |
| | |
| | | protected function generateIconCSS(): string |
| | | { |
| | | $css = ''; |
| | | $this->mergeUsedIcons(); |
| | | |
| | | foreach ($this->usedIcons as $style => $icons) { |
| | | $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : ''; |
| | | foreach ($icons as $icon) { |
| | | |
| | | $svg = $this->getEncodedSVG($icon, $style); |
| | | if ($svg !== '') { |
| | | $css .= ".icon-{$icon}{$styleClass}{"; |
| | |
| | | return $this->minifyCss($css); |
| | | } |
| | | |
| | | protected function mergeUsedIcons(array|bool $oldIcons = true, array|bool $newIcons = true):array |
| | | { |
| | | $set = false; |
| | | if ($oldIcons === true) { |
| | | $oldIcons = $this->usedIcons; |
| | | $set = true; |
| | | } |
| | | if ($newIcons === true) { |
| | | $history = $this->getVersionHistory(); |
| | | $newIcons = (count($history) > 0) ? $history[0]['iconList'] : []; |
| | | } |
| | | foreach ($newIcons as $style => $icons) { |
| | | if (!isset($oldIcons[$style])) { |
| | | //Style doesn't exist in previous set, add the whole thing |
| | | $oldIcons[$style] = $icons; |
| | | } else { |
| | | $oldIcons[$style] = array_unique( |
| | | array_merge($oldIcons[$style], $icons) |
| | | ); |
| | | } |
| | | } |
| | | if ($set) { |
| | | $this->usedIcons = $oldIcons; |
| | | update_option(BASE.'usedIcons', $oldIcons); |
| | | } |
| | | return $oldIcons; |
| | | } |
| | | |
| | | protected function minifyCSS(string $css): string |
| | | { |
| | | // Remove comments |
| | |
| | | public function clearIconCache(): void |
| | | { |
| | | delete_option(BASE . 'icon_usage_list'); // Clear DB option |
| | | delete_option(BASE.'usedIcons'); |
| | | delete_option(BASE.'includeIcons'); |
| | | delete_option(BASE.'iconMap'); |
| | | $this->cache->delete('icon_styles_css'); |
| | | CacheManager::updateTimestamp('icons'); |
| | | } |
| | | |
| | | protected function archiveCurrentVersion(string $css): void |
| | | { |
| | | $history = $this->getVersionHistory(); |
| | | |
| | | $icon_count = 0; |
| | | foreach ($this->usedIcons as $style => $icons) { |
| | | $icon_count += count($icons); |
| | | } |
| | | |
| | | $newEntry = [ |
| | | 'css' => $css, |
| | | 'iconList' => $this->usedIcons, |
| | | 'timestamp' => time(), |
| | | 'icon_count' => $icon_count, |
| | | 'size' => strlen($css), |
| | | 'size_formatted' => size_format(strlen($css), 2) |
| | | ]; |
| | | |
| | | array_unshift($history, $newEntry); |
| | | |
| | | if (count($history) > self::MAX_VERSIONS) { |
| | | $history = array_slice($history, 0, self::MAX_VERSIONS); |
| | | } |
| | | |
| | | update_option(BASE.'icon_css_history', $history); |
| | | } |
| | | |
| | | public function getVersionHistory(): array |
| | | { |
| | | return get_option(BASE.'icon_css_history', []); |
| | | } |
| | | |
| | | public function restoreVersion(int $timestamp): bool |
| | | { |
| | | $history = $this->getVersionHistory(); |
| | | |
| | | foreach ($history as $entry) { |
| | | if ($entry['timestamp'] === $timestamp) { |
| | | $css_path = JVB_DIR . '/assets/css/icons.css'; |
| | | |
| | | // Archive current before restoring |
| | | $current_css = file_get_contents($css_path); |
| | | if ($current_css !== false) { |
| | | $this->archiveCurrentVersion($current_css); |
| | | } |
| | | |
| | | // Restore the version |
| | | if (file_put_contents($css_path, $entry['css']) !== false) { |
| | | $this->usedIcons = $entry['iconList']; |
| | | update_option(BASE.'usedIcons', $this->usedIcons); |
| | | CacheManager::updateTimestamp('icons'); |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | error_log("[IconsManager] Version {$timestamp} not found in history"); |
| | | return false; |
| | | } |
| | | |
| | | public function forceRefresh(): void |
| | | { |
| | | $this->clearIconCache(); |
| | | update_option(BASE.'icons_needs_update', true); |
| | | CacheManager::updateTimestamp('icons'); |
| | | } |
| | | |
| | | public function mergeVersions(array $timestamps): bool |
| | | { |
| | | if (empty($timestamps)) { |
| | | return false; |
| | | } |
| | | |
| | | $history = get_option(BASE.'icon_css_history', []); |
| | | $merged_icons = []; |
| | | // Collect icons from selected versions |
| | | foreach ($history as $entry) { |
| | | if (in_array($entry['timestamp'], $timestamps)) { |
| | | foreach ($entry['iconList'] as $style => $icons) { |
| | | if (!isset($merged_icons[$style])) { |
| | | $merged_icons[$style] = []; |
| | | } |
| | | // Merge and keep unique |
| | | $merged_icons[$style] = array_unique( |
| | | array_merge($merged_icons[$style], $icons) |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (empty($merged_icons)) { |
| | | error_log('[IconsManager] No icons found in selected versions'); |
| | | return false; |
| | | } |
| | | |
| | | // Archive current version |
| | | $current_css = file_get_contents(JVB_DIR . '/assets/css/icons.css'); |
| | | if ($current_css !== false) { |
| | | $this->archiveCurrentVersion($current_css); |
| | | } |
| | | |
| | | // Update used icons and regenerate |
| | | $this->usedIcons = $merged_icons; |
| | | update_option(BASE.'usedIcons', $this->usedIcons); |
| | | |
| | | // Force regeneration |
| | | $this->regenerateCSS(); |
| | | |
| | | return true; |
| | | } |
| | | } |