Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
inc/managers/IconsManager.php
@@ -17,6 +17,7 @@
   protected array $customIcons = [];
   protected array $usedIcons = [];
   protected array $map = [];
   protected const MAX_VERSIONS = 5;
   /**
    * Get singleton instance
@@ -31,6 +32,7 @@
   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';
@@ -44,36 +46,35 @@
      ]);
      $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',
@@ -120,6 +121,7 @@
            'file-txt',
            'file-xls',
         ];
         $check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
         foreach ($check as $constant) {
            foreach ($constant as $key => $value) {
@@ -129,20 +131,77 @@
            }
         }
         $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
@@ -173,7 +232,8 @@
    */
   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']);
   }
@@ -192,7 +252,15 @@
   {
      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');
@@ -228,24 +296,23 @@
    */
   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'];
      }
@@ -264,13 +331,24 @@
    */
   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);
@@ -357,7 +435,7 @@
      wp_enqueue_style(
         'jvb-icons',
         JVB_URL.'assets/css/icons.css',
         JVB_CHILD_URL.'assets/css/icons.css',
         [],
         $timestamp
      );
@@ -369,10 +447,11 @@
   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}{";
@@ -384,6 +463,34 @@
      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
@@ -428,7 +535,123 @@
   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;
   }
}