From 601cf771703c82cd21b4f21898b467725d2dd277 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 06:46:47 -0500 Subject: [PATCH] fix: address PR review issues and add configurable slide transitions Fix CSS injection on heroHeight with regex validation, add missing language keys to .sys.ini files, fix plugin manifest languages folder attribute and display name, narrow catch to \Exception with logging, add error handling to pkg_script auto-enable, fix mobile CSS for color/gradient modes, add SPDX headers, remove dead code, and update stale file path headers. Add configurable transition type for image slideshow: crossfade, slide, fade-to-black, and zoom (Ken Burns). Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/mod_mokojoomhero.ini | 8 +++ .../language/en-GB/mod_mokojoomhero.sys.ini | 24 +++++++- .../language/en-US/mod_mokojoomhero.ini | 8 +++ .../language/en-US/mod_mokojoomhero.sys.ini | 24 +++++++- .../media/css/mod_mokojoomhero.css | 55 +++++++++++++++++-- .../media/js/mod_mokojoomhero.js | 55 +++++++++++++++---- .../mod_mokojoomhero/mod_mokojoomhero.php | 10 +++- .../mod_mokojoomhero/mod_mokojoomhero.xml | 15 ++++- src/packages/mod_mokojoomhero/script.php | 1 + .../mod_mokojoomhero/tmpl/default.php | 7 +-- .../plg_system_mokojoomhero/mokojoomhero.xml | 12 ++-- .../src/Extension/MokoJoomHero.php | 8 ++- src/pkg_script.php | 38 +++++++++---- 13 files changed, 220 insertions(+), 45 deletions(-) diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini index 2d480a4..3accf02 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.ini @@ -27,6 +27,14 @@ MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour" MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" + ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)." diff --git a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini index 0b8aeff..b4cb5e6 100644 --- a/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-GB/mod_mokojoomhero.sys.ini @@ -21,10 +21,20 @@ MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white backgro ; Hero mode MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode" -MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file." +MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid colour, or a gradient." MOD_MOKOJOOMHERO_MODE_IMAGES="Images" MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)" MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" +MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour" +MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" + +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" @@ -50,6 +60,18 @@ MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." +; Solid colour background +MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Colour" +MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background colour for the hero section." + +; Gradient background +MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Colour" +MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting colour of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Colour" +MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending colour of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle" +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)." + ; Hero height MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)." diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini index a6f1ba4..2c4ae6d 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.ini @@ -27,6 +27,14 @@ MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color" MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" + ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)." diff --git a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini index 18cc2ab..9bf627a 100644 --- a/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini +++ b/src/packages/mod_mokojoomhero/language/en-US/mod_mokojoomhero.sys.ini @@ -21,10 +21,20 @@ MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white backgro ; Hero mode MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode" -MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file." +MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid color, or a gradient." MOD_MOKOJOOMHERO_MODE_IMAGES="Images" MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)" MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video" +MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color" +MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient" + +; Transition type +MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type" +MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides." +MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade" +MOD_MOKOJOOMHERO_FADE_SLIDE="Slide" +MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black" +MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)" ; Image settings MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder" @@ -50,6 +60,18 @@ MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle" MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)." +; Solid color background +MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Color" +MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background color for the hero section." + +; Gradient background +MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Color" +MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting color of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Color" +MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending color of the gradient." +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle" +MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)." + ; Hero height MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height" MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)." diff --git a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css index ffda708..7ffda51 100644 --- a/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css +++ b/src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css @@ -6,9 +6,9 @@ * DEFGROUP: MokoJoomHero.Module.Assets * INGROUP: MokoJoomHero.Module * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero - * PATH: /src/css/template.css + * PATH: /src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css * VERSION: 01.04.01 - * BRIEF: Hero module stylesheet — slideshow, video background, overlay + * BRIEF: Hero module stylesheet — slideshow, video, colour/gradient, overlay, card, mute toggle, responsive */ /* ============================================================ @@ -31,7 +31,7 @@ } /* ============================================================ - Image slides + Image slides — base ============================================================ */ .mokojoomhero__slide { position: absolute; @@ -40,13 +40,51 @@ background-position: center; background-repeat: no-repeat; opacity: 0; - transition: opacity 1s ease; } .mokojoomhero__slide--active { opacity: 1; } +/* ── Crossfade (default) ── */ +.mokojoomhero[data-transition="crossfade"] .mokojoomhero__slide { + transition: opacity 1s ease; +} + +/* ── Slide ── */ +.mokojoomhero[data-transition="slide"] .mokojoomhero__slide { + opacity: 1; + transform: translateX(100%); + transition: transform 0.8s ease; +} + +.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--active { + transform: translateX(0); +} + +.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--exit { + transform: translateX(-100%); +} + +/* ── Fade to black ── */ +.mokojoomhero[data-transition="fade-black"] .mokojoomhero__slide { + transition: opacity 0.6s ease; +} + +/* ── Zoom (Ken Burns) ── */ +.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide { + transition: opacity 1s ease; +} + +.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active { + animation: mokojoomhero-zoom 8s ease forwards; +} + +@keyframes mokojoomhero-zoom { + from { transform: scale(1); } + to { transform: scale(1.08); } +} + /* ============================================================ Video background ============================================================ */ @@ -186,8 +224,17 @@ iframe.mokojoomhero__video { display: none; } + /* Keep colour/gradient backgrounds visible on mobile */ + .mokojoomhero__color { + position: relative; + } + .mokojoomhero__overlay { padding: 1rem; + } + + /* Only clear overlay background when media is hidden (image/video modes) */ + .mokojoomhero:not(:has(.mokojoomhero__color)) .mokojoomhero__overlay { background-color: transparent !important; } diff --git a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js index c5ddbe8..17dbfdc 100644 --- a/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js +++ b/src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js @@ -7,9 +7,9 @@ * DEFGROUP: MokoJoomHero.Module.Assets * INGROUP: MokoJoomHero.Module * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero - * PATH: /src/js/template.js + * PATH: /src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js * VERSION: 01.04.01 - * BRIEF: Hero module JavaScript — image slideshow crossfade + * BRIEF: Hero module JavaScript — slideshow crossfade, video viewport control, mute toggle */ 'use strict'; @@ -22,23 +22,54 @@ document.addEventListener('DOMContentLoaded', function () { // ── Image slideshow ── document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) { - var slides = hero.querySelectorAll('.mokojoomhero__slide'); - var interval = parseInt(hero.dataset.interval, 10) || 5000; - var current = 0; + var slides = hero.querySelectorAll('.mokojoomhero__slide'); + var interval = parseInt(hero.dataset.interval, 10) || 5000; + var transition = hero.dataset.transition || 'crossfade'; + var current = 0; if (slides.length < 2) { return; } - setInterval(function () { - slides[current].classList.remove('mokojoomhero__slide--active'); - slides[current].setAttribute('aria-hidden', 'true'); - + function advanceSlide() { + var prev = current; current = (current + 1) % slides.length; - slides[current].classList.add('mokojoomhero__slide--active'); - slides[current].setAttribute('aria-hidden', 'false'); - }, interval); + if (transition === 'slide') { + slides[prev].classList.add('mokojoomhero__slide--exit'); + slides[prev].classList.remove('mokojoomhero__slide--active'); + slides[prev].setAttribute('aria-hidden', 'true'); + + slides[current].classList.add('mokojoomhero__slide--active'); + slides[current].setAttribute('aria-hidden', 'false'); + + // Reset exiting slide after transition completes + setTimeout(function () { + slides[prev].classList.remove('mokojoomhero__slide--exit'); + }, 800); + + } else if (transition === 'fade-black') { + // Phase 1: fade out current + slides[prev].classList.remove('mokojoomhero__slide--active'); + slides[prev].setAttribute('aria-hidden', 'true'); + + // Phase 2: fade in next after a brief black gap + setTimeout(function () { + slides[current].classList.add('mokojoomhero__slide--active'); + slides[current].setAttribute('aria-hidden', 'false'); + }, 600); + + } else { + // Crossfade and zoom use the same JS logic + slides[prev].classList.remove('mokojoomhero__slide--active'); + slides[prev].setAttribute('aria-hidden', 'true'); + + slides[current].classList.add('mokojoomhero__slide--active'); + slides[current].setAttribute('aria-hidden', 'false'); + } + } + + setInterval(advanceSlide, interval); }); // ── Pause/resume videos when out of viewport ── diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php index e264f9e..4ae2aa1 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.php +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.php @@ -6,6 +6,7 @@ * * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GPL-3.0-or-later + * SPDX-License-Identifier: GPL-3.0-or-later */ defined('_JEXEC') or die; @@ -33,8 +34,14 @@ $heroMode = $params->get('heroMode', 'images'); $imageFolder = $params->get('imageFolder', 'images/heroes'); $imageCount = (int) $params->get('imageCount', 5); $slideInterval = (int) $params->get('slideInterval', 5000); +$fadeType = $params->get('fadeType', 'crossfade'); $videoFile = $params->get('videoFile', ''); $heroHeight = $params->get('heroHeight', '60vh'); + +// Validate heroHeight to prevent CSS injection +if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) { + $heroHeight = '60vh'; +} $overlayColor = $params->get('overlayColor', '#000000'); $overlayOpacity = (float) $params->get('overlayOpacity', 0.5); $textAlign = $params->get('textAlign', 'center'); @@ -98,7 +105,4 @@ if ($heroMode === 'localvideo' && $localVideoFile) { } } -// Module content from the editor (overlay text) -$content = $module->content ?? ''; - require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default')); diff --git a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml index 36a9544..3e47e1b 100644 --- a/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml +++ b/src/packages/mod_mokojoomhero/mod_mokojoomhero.xml @@ -10,7 +10,7 @@ DEFGROUP: MokoJoomHero.Module INGROUP: MokoJoomHero REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero - PATH: /src/mod_mokojoomhero.xml + PATH: /src/packages/mod_mokojoomhero/mod_mokojoomhero.xml VERSION: 01.00.20 BRIEF: Joomla module manifest — random hero image with content overlay --> @@ -99,6 +99,19 @@ step="15" showon="heroMode:gradient" /> + + + + + + id; // Convert hex overlay colour to rgba @@ -47,6 +45,7 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8'); 1) : ?> data-slides="" data-interval="" + data-transition="" > diff --git a/src/packages/plg_system_mokojoomhero/mokojoomhero.xml b/src/packages/plg_system_mokojoomhero/mokojoomhero.xml index e8782d2..d03d5cc 100644 --- a/src/packages/plg_system_mokojoomhero/mokojoomhero.xml +++ b/src/packages/plg_system_mokojoomhero/mokojoomhero.xml @@ -7,7 +7,7 @@ * @license GNU General Public License version 3 or later; see LICENSE --> - plg_system_mokojoomhero + PLG_SYSTEM_MOKOJOOMHERO 01.04.01-dev 2026-06-02 Moko Consulting @@ -25,10 +25,10 @@ src - - language/en-GB/plg_system_mokojoomhero.ini - language/en-GB/plg_system_mokojoomhero.sys.ini - language/en-US/plg_system_mokojoomhero.ini - language/en-US/plg_system_mokojoomhero.sys.ini + + en-GB/plg_system_mokojoomhero.ini + en-GB/plg_system_mokojoomhero.sys.ini + en-US/plg_system_mokojoomhero.ini + en-US/plg_system_mokojoomhero.sys.ini diff --git a/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php b/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php index 810af7f..85be0eb 100644 --- a/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php +++ b/src/packages/plg_system_mokojoomhero/src/Extension/MokoJoomHero.php @@ -83,8 +83,12 @@ class MokoJoomHero extends CMSPlugin implements SubscriberInterface . 'for the MokoJoomHero update site.', 'warning' ); - } catch (\Throwable $e) { - // Don't break admin over a license check + } catch (\Exception $e) { + // Don't break admin over a license check — log for diagnostics + $this->getApplication()->getLogger()->warning( + 'MokoJoomHero license check failed: ' . $e->getMessage(), + ['exception' => $e] + ); } } } diff --git a/src/pkg_script.php b/src/pkg_script.php index 1901b3a..5976c07 100644 --- a/src/pkg_script.php +++ b/src/pkg_script.php @@ -16,7 +16,7 @@ use Joomla\CMS\Installer\InstallerAdapter; class Pkg_MokoJoomHeroInstallerScript { /** - * Called after install/update. + * Called after install/update — only enables the system plugin on fresh install. * * @param string $type Action type * @param InstallerAdapter $parent Installer adapter @@ -26,18 +26,34 @@ class Pkg_MokoJoomHeroInstallerScript public function postflight(string $type, InstallerAdapter $parent): void { if ($type === 'install') { - $db = Factory::getDbo(); + try { + $db = Factory::getDbo(); - // Enable the system plugin automatically on fresh install - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero')); + // Enable the system plugin automatically on fresh install + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero')); - $db->setQuery($query); - $db->execute(); + $db->setQuery($query); + $db->execute(); + + if ($db->getAffectedRows() === 0) { + Factory::getApplication()->enqueueMessage( + 'MokoJoomHero: The system plugin could not be auto-enabled. ' + . 'Please enable it manually in Extensions → Plugins.', + 'warning' + ); + } + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage( + 'MokoJoomHero: Failed to auto-enable system plugin: ' . $e->getMessage() + . ' — Please enable it manually in Extensions → Plugins.', + 'warning' + ); + } } } }