Compare commits
106 Commits
development
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2671043d | |||
| aa29f601c5 | |||
| 95f4fe998c | |||
| 3bdaeee40f | |||
| 23ab29afab | |||
| e870a0adfb | |||
| 21f4d203c7 | |||
| 8cf8472bec | |||
| 1708e6386b | |||
| a9399f9aef | |||
| a2dcf2f072 | |||
| 32a5dd2d0d | |||
| 7bd77d3b68 | |||
| f38b5180b7 | |||
| f1112c1762 | |||
| 692f7ab0ef | |||
| 636d568fe2 | |||
| f05c30b662 | |||
| 03e29b3945 | |||
| 8ced44cf1d | |||
| ed2bbebd3e | |||
| 11dbf4d785 | |||
| 96ae87b19d | |||
| 8cb2ea30a1 | |||
| 9a62d9d7cc | |||
| cf1fb26390 | |||
| a7cf29d220 | |||
| e978090bb8 | |||
| 7f7dd60a4a | |||
| e711bb4fa0 | |||
| ea9ea5c3a7 | |||
| 49f9a762df | |||
| a6e9302730 | |||
| ce498da605 | |||
| d46325e13b | |||
| 69362e0aca | |||
| 84b3b9acb1 | |||
| 9f0c01739f | |||
| a7eabcf9d7 | |||
| 724c4d61f4 | |||
| 35cb0a988c | |||
| 9e27c5c167 | |||
| f56bfa6c36 | |||
| f4dfcc4c7e | |||
| f9187cd816 | |||
| a9d4534f0b | |||
| a922ef3033 | |||
| 9fbc273658 | |||
| 95319c0513 | |||
| 10412c1120 | |||
| ab82a8f810 | |||
| 19db4cb637 | |||
| 28bc4f042a | |||
| 9de47e9fe2 | |||
| af3acc6fd2 | |||
| ad0c5bc51c | |||
| fdf07d833f | |||
| 765086d7a0 | |||
| 9e729c059d | |||
| d708ac995d | |||
| 8895ba9ffb | |||
| 2f88eb50fb | |||
| d22ba60eea | |||
| 52e362a769 | |||
| 012f8adc3d | |||
| 5e7050576a | |||
| 95bba21c91 | |||
| 998f62fcfb | |||
| ba5ae04755 | |||
| b3b410e190 | |||
| c38307f98b | |||
| c3a4a1f28d | |||
| 4d67a32cb0 | |||
| 6f9aa71573 | |||
| 4a18f46c68 | |||
| f5fdf6742f | |||
| fe3abf6ddb | |||
| 601cf77170 | |||
| 902321de47 | |||
| d085c79d9e | |||
| ae19e32407 | |||
| 61cd30471f | |||
| 46bfeaa2e1 | |||
| ea1391ab38 | |||
| fe84d02827 | |||
| cfe69452db | |||
| a465ee1d38 | |||
| 0742f056be | |||
| 1b74b9c8d7 | |||
| 518f8f8850 | |||
| fe908b68a2 | |||
| b85921697f | |||
| 77b39a2d52 | |||
| 351e86328f | |||
| 31de00c741 | |||
| 595bc94b4b | |||
| f1049fd289 | |||
| 79627c4f1d | |||
| 519c735e20 | |||
| bc31cad73a | |||
| c583ebd56e | |||
| f9543058df | |||
| 4b08e5d889 | |||
| 2f2832c661 | |||
| 69e75f50fb | |||
| 7df3c7afd7 |
@@ -6,10 +6,11 @@
|
|||||||
-->
|
-->
|
||||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||||
<identity>
|
<identity>
|
||||||
<name>Module - MokoJoomHero</name>
|
<name>MokoJoomHero</name>
|
||||||
|
<display-name>Package - MokoJoomHero</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>A Joomla Module designed to provide a random image from a folder with content on top as a Hero.</description>
|
<description>A Joomla Module designed to provide a random image from a folder with content on top as a Hero.</description>
|
||||||
<version>01.07.00</version>
|
<version>01.19.00</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -1,464 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.Joomla
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/joomla/update-server.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
|
||||||
#
|
|
||||||
# Writes updates.xml with multiple <update> entries:
|
|
||||||
# - <tag>stable</tag> on push to main (from auto-release)
|
|
||||||
# - <tag>rc</tag> on push to rc/**
|
|
||||||
# - <tag>development</tag> on push to dev or dev/**
|
|
||||||
#
|
|
||||||
# Joomla filters by user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: Update Joomla Update Server XML Feed
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update updates.xml
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
|
||||||
/tmp/mokostandards-api 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
|
||||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate updates.xml entry
|
|
||||||
id: update
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Auto-bump patch on all branches (dev, alpha, beta, rc)
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
|
|
||||||
if [ -n "$BUMPED" ]; then
|
|
||||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
|
|
||||||
git push 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine stability from branch or input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
|
|
||||||
STABILITY="development"
|
|
||||||
else
|
|
||||||
STABILITY="stable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Parse manifest (portable — no grep -P)
|
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No Joomla manifest found — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract fields using sed (works on all runners)
|
|
||||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
|
|
||||||
# Fallbacks
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
|
||||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
|
||||||
|
|
||||||
# Derive element if not in manifest: try XML filename, then repo name
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
case "$EXT_ELEMENT" in
|
|
||||||
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use manifest version if README version is empty
|
|
||||||
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
|
||||||
|
|
||||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
|
||||||
|
|
||||||
CLIENT_TAG=""
|
|
||||||
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
|
||||||
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
|
|
||||||
|
|
||||||
FOLDER_TAG=""
|
|
||||||
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
|
||||||
|
|
||||||
PHP_TAG=""
|
|
||||||
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
|
||||||
|
|
||||||
# Version suffix for non-stable
|
|
||||||
DISPLAY_VERSION="$VERSION"
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
|
||||||
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
|
||||||
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
|
||||||
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
|
||||||
|
|
||||||
# Each stability level has its own release tag
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) RELEASE_TAG="development" ;;
|
|
||||||
alpha) RELEASE_TAG="alpha" ;;
|
|
||||||
beta) RELEASE_TAG="beta" ;;
|
|
||||||
rc) RELEASE_TAG="release-candidate" ;;
|
|
||||||
*) RELEASE_TAG="v${MAJOR}" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
|
||||||
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
|
||||||
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# -- Build install packages (ZIP + tar.gz) --------------------
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ -d "$SOURCE_DIR" ]; then
|
|
||||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
|
||||||
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
|
||||||
|
|
||||||
cd "$SOURCE_DIR"
|
|
||||||
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
|
||||||
cd ..
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
|
||||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
|
||||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Ensure release exists on Gitea
|
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
|
||||||
# Create release
|
|
||||||
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/releases" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'tag_name': '${RELEASE_TAG}',
|
|
||||||
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
|
|
||||||
'body': '${STABILITY} release',
|
|
||||||
'prerelease': True,
|
|
||||||
'target_commitish': 'main'
|
|
||||||
}))")" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
# Delete existing assets with same name before uploading
|
|
||||||
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
|
||||||
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
|
|
||||||
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
|
||||||
import sys,json
|
|
||||||
assets = json.load(sys.stdin)
|
|
||||||
for a in assets:
|
|
||||||
if a['name'] == '${ASSET_FILE}':
|
|
||||||
print(a['id']); break
|
|
||||||
" 2>/dev/null || true)
|
|
||||||
if [ -n "$ASSET_ID" ]; then
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Upload both formats
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${PACKAGE_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${TAR_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
SHA256=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Build the new entry (canonical format matching release.yml) --
|
|
||||||
NEW_ENTRY=""
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
|
||||||
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
|
||||||
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
|
||||||
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
|
|
||||||
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </update>"
|
|
||||||
|
|
||||||
# -- Write new entry to temp file --------------------------------
|
|
||||||
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
|
||||||
|
|
||||||
# -- Merge into updates.xml ----------------------------------------
|
|
||||||
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
|
|
||||||
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
|
|
||||||
TARGETS=""
|
|
||||||
for entry in $CASCADE_MAP; do
|
|
||||||
key="${entry%%:*}"
|
|
||||||
vals="${entry#*:}"
|
|
||||||
if [ "$key" = "${STABILITY}" ]; then
|
|
||||||
TARGETS="$vals"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
|
|
||||||
|
|
||||||
echo "Cascade: ${STABILITY} → ${TARGETS}"
|
|
||||||
|
|
||||||
# Create updates.xml if missing
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
|
|
||||||
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
|
|
||||||
printf '%s\n' "<updates>" >> updates.xml
|
|
||||||
printf '%s\n' "</updates>" >> updates.xml
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update existing blocks or create missing ones
|
|
||||||
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
|
|
||||||
python3 << 'PYEOF'
|
|
||||||
import re, os
|
|
||||||
|
|
||||||
targets = os.environ["PY_TARGETS"].split(",")
|
|
||||||
version = os.environ["PY_VERSION"]
|
|
||||||
date = os.environ["PY_DATE"]
|
|
||||||
|
|
||||||
with open("updates.xml") as f:
|
|
||||||
content = f.read()
|
|
||||||
with open("/tmp/new_entry.xml") as f:
|
|
||||||
new_entry_template = f.read()
|
|
||||||
|
|
||||||
for tag in targets:
|
|
||||||
tag = tag.strip()
|
|
||||||
# Build entry with this tag's name
|
|
||||||
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
|
|
||||||
|
|
||||||
# Try to find existing block (handles both single-line and multi-line <tags>)
|
|
||||||
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
|
|
||||||
match = re.search(block_pattern, content, re.DOTALL)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# Update in place — replace entire block
|
|
||||||
content = content.replace(match.group(1), new_entry.strip())
|
|
||||||
print(f" UPDATED: <tag>{tag}</tag> → {version}")
|
|
||||||
else:
|
|
||||||
# Create — insert before </updates>
|
|
||||||
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
|
|
||||||
print(f" CREATED: <tag>{tag}</tag> → {version}")
|
|
||||||
|
|
||||||
# Clean up excessive blank lines
|
|
||||||
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
||||||
|
|
||||||
with open("updates.xml", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
# Commit
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Sync updates.xml to main (for non-main branches) ----------------------
|
|
||||||
- name: Sync updates.xml to main
|
|
||||||
if: github.ref_name != 'main'
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
|
||||||
CONTENT=$(base64 -w0 updates.xml)
|
|
||||||
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/contents/updates.xml" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'content': '${CONTENT}',
|
|
||||||
'sha': '${FILE_SHA}',
|
|
||||||
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
|
|
||||||
'branch': 'main'
|
|
||||||
}))")" > /dev/null 2>&1 \
|
|
||||||
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|
|
||||||
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# -- Permission check: admin or maintain role required --------
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
|
||||||
# VERSION: 09.02.00
|
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
|
||||||
|
|
||||||
name: "Universal: Auto Version Bump"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- rc
|
|
||||||
- 'feature/**'
|
|
||||||
- 'patch/**'
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bump:
|
|
||||||
name: Version Bump
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Bump version
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/version_auto_bump.php \
|
|
||||||
--path . --branch "${GITHUB_REF_NAME}" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
@@ -102,13 +102,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--skip-update-stream
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -131,6 +132,19 @@ jobs:
|
|||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
@@ -154,7 +168,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--skip-update-stream
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Maintenance
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
|
||||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
|
||||||
# VERSION: 02.00.00
|
|
||||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | CASCADE MAIN → ALL BRANCHES |
|
|
||||||
# +========================================================================+
|
|
||||||
# | |
|
|
||||||
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
|
||||||
# | |
|
|
||||||
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
|
|
||||||
# | 2. For each: create PR (main → branch), auto-merge if clean |
|
|
||||||
# | 3. On conflict: leave PR open for manual resolution |
|
|
||||||
# | |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Universal: Cascade Main → Dev"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cascade:
|
|
||||||
name: Cascade main → branches
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip cascade]')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Discover target branches
|
|
||||||
id: branches
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Fetch all branches (paginated)
|
|
||||||
PAGE=1
|
|
||||||
ALL_BRANCHES=""
|
|
||||||
while true; do
|
|
||||||
BATCH=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/branches?page=${PAGE}&limit=50" \
|
|
||||||
| jq -r '.[].name // empty')
|
|
||||||
[ -z "$BATCH" ] && break
|
|
||||||
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
|
|
||||||
PAGE=$((PAGE + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
|
|
||||||
TARGETS=""
|
|
||||||
for BRANCH in $ALL_BRANCHES; do
|
|
||||||
case "$BRANCH" in
|
|
||||||
dev|dev/*|rc/*|beta/*|alpha/*)
|
|
||||||
TARGETS="$TARGETS $BRANCH"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
|
|
||||||
|
|
||||||
if [ -z "$TARGETS" ]; then
|
|
||||||
echo "targets=" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ℹ️ No cascade target branches found"
|
|
||||||
else
|
|
||||||
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
|
||||||
COUNT=$(echo "$TARGETS" | wc -w)
|
|
||||||
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Cascade to all target branches
|
|
||||||
if: steps.branches.outputs.targets != ''
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
|
||||||
TARGETS="${{ steps.branches.outputs.targets }}"
|
|
||||||
|
|
||||||
SUCCESS=0
|
|
||||||
CONFLICTS=0
|
|
||||||
SKIPPED=0
|
|
||||||
FAILED=0
|
|
||||||
|
|
||||||
for BRANCH in $TARGETS; do
|
|
||||||
echo ""
|
|
||||||
echo "═══ main → ${BRANCH} ═══"
|
|
||||||
|
|
||||||
# Check if branch is already up to date
|
|
||||||
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
|
||||||
RESPONSE=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/compare/${ENCODED_BRANCH}...main")
|
|
||||||
|
|
||||||
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
|
||||||
|
|
||||||
if [ "$AHEAD" -eq 0 ]; then
|
|
||||||
echo " ✅ Already up to date"
|
|
||||||
SKIPPED=$((SKIPPED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ℹ️ main is ${AHEAD} commit(s) ahead"
|
|
||||||
|
|
||||||
# Check for existing cascade PR
|
|
||||||
EXISTING=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
|
||||||
|
|
||||||
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
|
||||||
PR_NUMBER=""
|
|
||||||
|
|
||||||
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
|
||||||
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
|
||||||
echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
|
|
||||||
else
|
|
||||||
# Create cascade PR
|
|
||||||
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
|
||||||
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
|
|
||||||
\"head\": \"main\",
|
|
||||||
\"base\": \"${BRANCH}\"
|
|
||||||
}" \
|
|
||||||
"${API}/pulls")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
|
|
||||||
BODY=$(echo "$PR_RESPONSE" | sed '$d')
|
|
||||||
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
|
|
||||||
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
|
|
||||||
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ✅ Created PR #${PR_NUMBER}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try auto-merge
|
|
||||||
PR_DATA=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/pulls/${PR_NUMBER}")
|
|
||||||
|
|
||||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
|
||||||
|
|
||||||
if [ "$MERGEABLE" != "true" ]; then
|
|
||||||
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
|
|
||||||
CONFLICTS=$((CONFLICTS + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"Do\": \"merge\",
|
|
||||||
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
|
||||||
\"delete_branch_after_merge\": false
|
|
||||||
}" \
|
|
||||||
"${API}/pulls/${PR_NUMBER}/merge")
|
|
||||||
|
|
||||||
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
|
||||||
|
|
||||||
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
|
||||||
echo " ✅ Merged — ${BRANCH} is in sync"
|
|
||||||
SUCCESS=$((SUCCESS + 1))
|
|
||||||
else
|
|
||||||
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
|
||||||
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
|
||||||
CONFLICTS=$((CONFLICTS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════"
|
|
||||||
echo " ✅ Merged: ${SUCCESS}"
|
|
||||||
echo " ⚠️ Conflicts: ${CONFLICTS}"
|
|
||||||
echo " ⏭️ Up to date: ${SKIPPED}"
|
|
||||||
echo " ❌ Failed: ${FAILED}"
|
|
||||||
echo "════════════════════════════════════════"
|
|
||||||
|
|
||||||
if [ "$FAILED" -gt 0 ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 01.07.00
|
# VERSION: 01.19.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
+508
-236
@@ -1,236 +1,508 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
name: "Universal: PR Check"
|
name: "Universal: PR Check"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Branch Policy ──────────────────────────────────────────────────────
|
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||||
branch-policy:
|
branch-policy:
|
||||||
name: Branch Policy
|
name: Branch Policy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check branch merge target
|
- name: Check branch merge target
|
||||||
run: |
|
run: |
|
||||||
HEAD="${{ github.head_ref }}"
|
HEAD="${{ github.head_ref }}"
|
||||||
BASE="${{ github.base_ref }}"
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
echo "PR: ${HEAD} → ${BASE}"
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
ALLOWED=true
|
ALLOWED=true
|
||||||
REASON=""
|
REASON=""
|
||||||
|
|
||||||
case "$HEAD" in
|
case "$HEAD" in
|
||||||
feature/*|feat/*)
|
feature/*|feat/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Feature branches must target 'dev', not '${BASE}'"
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
fix/*|bugfix/*)
|
fix/*|bugfix/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
patch/*)
|
patch/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
hotfix/*)
|
hotfix/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rc)
|
rc)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dev)
|
dev)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$ALLOWED" = false ]; then
|
if [ "$ALLOWED" = false ]; then
|
||||||
echo "::error::${REASON}"
|
echo "::error::${REASON}"
|
||||||
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Check for merge conflict markers
|
||||||
id: platform
|
run: |
|
||||||
run: |
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
if [ -n "$CONFLICTS" ]; then
|
||||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
echo "::error::Merge conflict markers found in source files"
|
||||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Setup PHP
|
exit 1
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
fi
|
||||||
run: |
|
echo "No conflict markers found"
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
- name: Detect platform
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
id: platform
|
||||||
fi
|
run: |
|
||||||
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
- name: PHP syntax check
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
run: |
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
ERRORS=0
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
- name: Setup PHP
|
||||||
ERRORS=$((ERRORS + 1))
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
fi
|
run: |
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
if ! command -v php &> /dev/null; then
|
||||||
echo "PHP lint: ${ERRORS} error(s)"
|
sudo apt-get update -qq
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
- name: Validate platform manifest
|
|
||||||
run: |
|
- name: PHP syntax check
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
case "$PLATFORM" in
|
run: |
|
||||||
joomla)
|
ERRORS=0
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
while IFS= read -r -d '' file; do
|
||||||
if [ -z "$MANIFEST" ]; then
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
ERRORS=$((ERRORS + 1))
|
||||||
exit 0
|
fi
|
||||||
fi
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
echo "Manifest: ${MANIFEST}"
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
if command -v php &> /dev/null; then
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
|
||||||
fi
|
- name: Joomla JEXEC guard check
|
||||||
for ELEMENT in name version description; do
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
run: |
|
||||||
done
|
ERRORS=0
|
||||||
echo "Joomla manifest valid"
|
while IFS= read -r -d '' file; do
|
||||||
;;
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
dolibarr)
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
# Check first 10 lines for JEXEC or JPATH guard
|
||||||
if [ -z "$MOD_FILE" ]; then
|
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
||||||
echo "::error::No mod*.class.php found"
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
exit 1
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
echo "Dolibarr module: ${MOD_FILE}"
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
;;
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
*)
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
echo "Generic platform — no manifest validation"
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
esac
|
exit 1
|
||||||
|
fi
|
||||||
- name: Check update stream format
|
echo "JEXEC guard: OK"
|
||||||
run: |
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
- name: Joomla directory listing protection
|
||||||
case "$PLATFORM" in
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
joomla)
|
run: |
|
||||||
if [ -f "updates.xml" ]; then
|
MISSING=0
|
||||||
if command -v php &> /dev/null; then
|
SOURCE_DIR="src"
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
fi
|
while IFS= read -r dir; do
|
||||||
echo "updates.xml valid"
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
fi
|
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
||||||
;;
|
MISSING=$((MISSING + 1))
|
||||||
dolibarr)
|
fi
|
||||||
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
||||||
;;
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
esac
|
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Check changelog has unreleased entry
|
fi
|
||||||
run: |
|
echo "Directory protection: ${MISSING} missing (advisory)"
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
|
||||||
echo "::warning::No CHANGELOG.md found"
|
- name: Joomla script file and asset checks
|
||||||
exit 0
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
fi
|
run: |
|
||||||
# Check for content under [Unreleased] section
|
ERRORS=0
|
||||||
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
exit 1
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
fi
|
|
||||||
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
# Check scriptfile exists if declared
|
||||||
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
if [ -n "$SCRIPTFILE" ]; then
|
||||||
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||||
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
||||||
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
ERRORS=$((ERRORS + 1))
|
||||||
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
else
|
||||||
exit 1
|
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
||||||
fi
|
fi
|
||||||
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
fi
|
||||||
|
|
||||||
- name: Verify package source
|
# Require joomla.asset.json and validate it
|
||||||
run: |
|
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
SOURCE_DIR="src"
|
if [ -z "$ASSET_JSON" ]; then
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
ERRORS=$((ERRORS + 1))
|
||||||
echo "::warning::No src/ or htdocs/ directory"
|
else
|
||||||
exit 0
|
if command -v php &> /dev/null; then
|
||||||
fi
|
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
echo "::error::joomla.asset.json is not valid JSON"
|
||||||
echo "Source: ${FILE_COUNT} files"
|
ERRORS=$((ERRORS + 1))
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
}
|
||||||
|
fi
|
||||||
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
echo "joomla.asset.json: valid"
|
||||||
pre-release:
|
fi
|
||||||
name: Build RC Package
|
|
||||||
runs-on: ubuntu-latest
|
# Validate all XML files in src/ are well-formed
|
||||||
needs: [branch-policy, validate]
|
XML_ERRORS=0
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
steps:
|
while IFS= read -r -d '' xmlfile; do
|
||||||
- name: Trigger RC pre-release
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
||||||
env:
|
XML_ERRORS=$((XML_ERRORS + 1))
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
fi
|
||||||
REPO: ${{ github.repository }}
|
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
||||||
BRANCH: ${{ github.head_ref }}
|
fi
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
if [ "$XML_ERRORS" -gt 0 ]; then
|
||||||
run: |
|
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
||||||
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
ERRORS=$((ERRORS + 1))
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
else
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "XML well-formedness: OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
echo "Joomla asset checks: OK"
|
||||||
|
|
||||||
|
- name: Validate platform manifest
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
for ELEMENT in name version description; do
|
||||||
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
|
done
|
||||||
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
|
if [ -n "$RAW_URLS" ]; then
|
||||||
|
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||||
|
echo "$RAW_URLS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Joomla manifest valid"
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MOD_FILE" ]; then
|
||||||
|
echo "::error::No mod*.class.php found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Generic platform — no manifest validation"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Check update stream format
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
echo "updates.xml valid"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Validate Joomla language files
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Require both en-GB and en-US language directories
|
||||||
|
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LANG_ROOT" ]; then
|
||||||
|
echo "No language/ directory found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||||
|
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that en-GB and en-US have matching .ini files
|
||||||
|
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||||
|
[ ! -f "$GB_INI" ] && continue
|
||||||
|
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||||
|
if [ ! -f "$US_INI" ]; then
|
||||||
|
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||||
|
[ ! -f "$US_INI" ] && continue
|
||||||
|
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||||
|
if [ ! -f "$GB_INI" ]; then
|
||||||
|
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all .ini language files
|
||||||
|
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||||
|
if [ -z "$INI_FILES" ]; then
|
||||||
|
echo "No .ini language files found"
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||||
|
|
||||||
|
for FILE in $INI_FILES; do
|
||||||
|
FNAME=$(basename "$FILE")
|
||||||
|
LINENUM=0
|
||||||
|
SEEN_KEYS=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
LINENUM=$((LINENUM + 1))
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
echo "$line" | grep -qE '^\s*;' && continue
|
||||||
|
echo "$line" | grep -qE '^\s*$' && continue
|
||||||
|
|
||||||
|
# Must match KEY="VALUE" format
|
||||||
|
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key and check for duplicates
|
||||||
|
KEY=$(echo "$line" | sed 's/=.*//')
|
||||||
|
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
SEEN_KEYS="${SEEN_KEYS}
|
||||||
|
${KEY}"
|
||||||
|
done < "$FILE"
|
||||||
|
|
||||||
|
echo " ${FILE}: checked ${LINENUM} lines"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cross-check en-GB vs en-US key consistency
|
||||||
|
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||||
|
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||||
|
[ ! -f "$GB_FILE" ] && continue
|
||||||
|
FNAME=$(basename "$GB_FILE")
|
||||||
|
US_FILE="$US_DIR/$FNAME"
|
||||||
|
[ ! -f "$US_FILE" ] && continue
|
||||||
|
|
||||||
|
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||||
|
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
# Keys in en-GB but not en-US
|
||||||
|
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_US" ]; then
|
||||||
|
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||||
|
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keys in en-US but not en-GB
|
||||||
|
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_GB" ]; then
|
||||||
|
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||||
|
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Language File Validation"
|
||||||
|
echo "| Metric | Count |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||||
|
echo "| Errors | ${ERRORS} |"
|
||||||
|
echo "| Warnings | ${WARNINGS} |"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||||
|
|
||||||
|
- name: Check changelog has unreleased entry
|
||||||
|
run: |
|
||||||
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
|
echo "::warning::No CHANGELOG.md found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Check for content under [Unreleased] section
|
||||||
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
|
- name: Verify package source
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source: ${FILE_COUNT} files"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger RC pre-release
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
|
report-issues:
|
||||||
|
name: Report Issues
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.validate.result == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
|
||||||
# VERSION: 05.01.00
|
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Pre-release channel'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- release-candidate
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
|
||||||
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Read current version (bump already handled by push workflow)
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
|
||||||
|
|
||||||
# Strip any existing suffix from version before applying stability
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Verify version consistency across all files
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Update VERSION variable with suffix
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Commit version bump
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Auto-detect element via manifest_element.php
|
|
||||||
php ${MOKO_CLI}/manifest_element.php \
|
|
||||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
|
||||||
--repo "${GITEA_REPO}" --github-output
|
|
||||||
|
|
||||||
# Read back element outputs
|
|
||||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
|
||||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
id: release
|
|
||||||
run: |
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
|
||||||
|
|
||||||
- name: Build package and upload
|
|
||||||
id: package
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
php ${MOKO_CLI}/release_package.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml -- skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Sync updates.xml to all branches"
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
|
|
||||||
for BRANCH in main dev; do
|
|
||||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
|
||||||
echo "Syncing updates.xml -> ${BRANCH}"
|
|
||||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
|
||||||
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
|
||||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
|
||||||
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
|
||||||
fi
|
|
||||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
|
||||||
--stability "${{ steps.meta.outputs.stability }}" \
|
|
||||||
--token "${TOKEN}" \
|
|
||||||
--api-base "${API_BASE}"
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,312 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/update-server.yml
|
|
||||||
# VERSION: 05.00.00
|
|
||||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
|
||||||
#
|
|
||||||
# Thin wrapper around moko-platform CLI tools.
|
|
||||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
|
||||||
#
|
|
||||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: "Update Server"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update Server
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
|
||||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
|
||||||
id: platform
|
|
||||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve stability and bump version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
|
|
||||||
# Configure git for bot pushes
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
# Auto-bump patch version
|
|
||||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
|
||||||
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Strip any existing suffix before applying stability
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
|
|
||||||
# Determine stability from branch or manual input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
else
|
|
||||||
STABILITY="development"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version suffix per stability stream
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
|
||||||
*) SUFFIX=""; TAG="stable" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Propagate version with stability suffix to all manifest files
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
|
||||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Re-read version (now includes suffix from version_set_platform)
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Commit version bump if changed
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Create release and upload package
|
|
||||||
id: package
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Create or update Gitea release
|
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
|
||||||
|
|
||||||
# Build package and upload
|
|
||||||
php ${MOKO_CLI}/release_package.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push updates.xml
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Sync updates.xml to main
|
|
||||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import base64, json, urllib.request, sys
|
|
||||||
with open('updates.xml', 'rb') as f:
|
|
||||||
content = base64.b64encode(f.read()).decode()
|
|
||||||
payload = json.dumps({
|
|
||||||
'content': content,
|
|
||||||
'sha': '${FILE_SHA}',
|
|
||||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
|
||||||
'branch': 'main'
|
|
||||||
}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/contents/updates.xml',
|
|
||||||
data=payload, method='PUT',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${GITEA_TOKEN}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
print('updates.xml synced to main')
|
|
||||||
except Exception as e:
|
|
||||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# Permission check: admin or maintain role required
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
|
||||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
+21
-27
@@ -1,38 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [01.19.00] --- 2026-06-04
|
||||||
## [01.07.00] --- 2026-05-30
|
|
||||||
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
## [01.18.00] --- 2026-06-04
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
## [01.17] - 2026-06-04
|
||||||
Version format: `XX.YY.ZZ` (zero-padded semver).
|
|
||||||
|
|
||||||
## [01.06.00] --- 2026-05-30
|
|
||||||
|
|
||||||
## [01.04.00] - 2026-05-30
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Local Video hero mode with Joomla Media Manager file picker (`localVideoFile` param)
|
- Download key support (`dlid`) in module manifest for update server authentication
|
||||||
- Install script (`script.php`) creates `images/heroes/` folder on install/update
|
- Auto-remove deprecated system plugin (`plg_system_mokojoomhero`) on install/upgrade
|
||||||
|
|
||||||
## [01.03.00] - 2026-05-30
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Configurable card fade-in delay with slide-up animation (`cardDelay` param, 0-5000ms) (#39)
|
|
||||||
- Video mute/unmute toggle button (`showMuteToggle` param) -- supports YouTube, Vimeo, and native video (#40)
|
|
||||||
|
|
||||||
## [01.02.00] - 2026-05-30
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- WebAsset registration: added `#style`/`#script` suffixes to preset dependencies (was causing `UnsatisfiedDependencyException`)
|
- Skip overlay background on solid colour and gradient hero modes — gradient was invisible behind 50% black overlay
|
||||||
- Asset URI resolution: use `extension/filename` pattern instead of `extension/folder/filename` (Joomla auto-inserts `css/`/`js/` folders)
|
|
||||||
- iframe video cover: split CSS into `<video>` (`object-fit: cover`) and `<iframe>` (oversize + centre-crop) rules since `object-fit` doesn't apply to iframes
|
|
||||||
- Card link styling: exclude `.btn` elements from `color: inherit` so buttons retain their own styles
|
|
||||||
|
|
||||||
### Added
|
## [01.14] - 2026-06-04
|
||||||
- Module title renders inside the hero card as `<h2>`, respecting Joomla's Show Title toggle
|
|
||||||
- IntersectionObserver pauses/resumes videos when the hero scrolls out of/into the viewport (YouTube, Vimeo, and native `<video>`)
|
### Changed
|
||||||
- YouTube embeds include `enablejsapi=1` for postMessage playback control
|
- Update server URL to new Gitea release system
|
||||||
|
- Use pretty name format for update server entry
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Add missing `<updateservers>` section to module manifest
|
||||||
|
|
||||||
|
## [01.13] - 2026-06-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Restructure from package extension back to standalone site module
|
||||||
|
- Remove system plugin (`plg_system_mokojoomhero`) and package wrapper
|
||||||
|
- Simplify build target for module ZIP
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository.
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**MokoJoomHero** -- A Joomla Module designed to provide a random image from a folder with content on top as a Hero.
|
**MokoJoomHero** -- Random hero image slideshow/video with content overlay for Joomla
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -32,20 +32,33 @@ composer install # Install PHP dependencies
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
This is a Joomla extension. Key directories:
|
This is a Joomla **site module** (`mod_mokojoomhero`).
|
||||||
- `src/` -- extension source (deployed to Joomla)
|
|
||||||
- `src/*.xml` -- manifest file (version, files, params)
|
- Random hero image slideshow or background video with content overlay
|
||||||
- `src/src/` or `src/services/` -- PHP classes
|
- Supports image folders, YouTube, Vimeo, local video, solid colour, gradient
|
||||||
- `src/language/` -- translation strings
|
- Configurable overlay, text alignment, card animation, parallax, content animations
|
||||||
- `src/media/` -- CSS/JS/images
|
- A/B testing with weighted variations, scheduling with start/end datetime
|
||||||
|
- Article content source, per-slide unique content via subform
|
||||||
|
|
||||||
|
### Key files
|
||||||
|
- `src/mod_mokojoomhero.xml` — module manifest
|
||||||
|
- `src/mod_mokojoomhero.php` — module entry point
|
||||||
|
- `src/tmpl/default.php` — template
|
||||||
|
- `src/media/` — CSS, JS, and web asset registry
|
||||||
|
- `src/language/` — en-GB and en-US translations
|
||||||
|
- `src/script.php` — install script (creates default image folder)
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
|
||||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
||||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
||||||
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
||||||
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
|
- **Minification**: handled at build time (CI)
|
||||||
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
||||||
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
- PHP 8.1+ minimum
|
||||||
|
- SPDX license headers on all PHP files
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
DEFGROUP:
|
DEFGROUP:
|
||||||
INGROUP: Project.Documentation
|
INGROUP: Project.Documentation
|
||||||
REPO:
|
REPO:
|
||||||
VERSION: 01.07.00
|
VERSION: 01.19.00
|
||||||
PATH: ./CODE_OF_CONDUCT.md
|
PATH: ./CODE_OF_CONDUCT.md
|
||||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||||
-->
|
-->
|
||||||
|
|||||||
+141
-108
@@ -1,128 +1,161 @@
|
|||||||
<!--
|
# Contributing to Moko Consulting Projects
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
## Branching Workflow
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
DEFGROUP: {{DEFGROUP}}
|
|
||||||
INGROUP: Project.Documentation
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoJoomHero
|
|
||||||
VERSION: 01.07.00
|
|
||||||
PATH: ./CONTRIBUTING.md
|
|
||||||
BRIEF: How to contribute; branch strategy, commit conventions, PR workflow, and release pipeline
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
Thank you for your interest in contributing to **MokoJoomHero**!
|
|
||||||
|
|
||||||
This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** — the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories.
|
|
||||||
|
|
||||||
## Branch Strategy
|
|
||||||
|
|
||||||
| Branch | Purpose | Deploys To |
|
|
||||||
|--------|---------|------------|
|
|
||||||
| `main` | Bleeding edge — all development merges here | CI only |
|
|
||||||
| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") |
|
|
||||||
| `version/XX.YY` | Stable frozen snapshot | Demo + RS servers |
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
2. Develop + test (dev server auto-deploys on push)
|
|
||||||
3. Open PR → main (squash merge only)
|
|
||||||
4. Auto-release (version branch + tag + GitHub Release created automatically)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Branch Naming
|
### Step by step
|
||||||
|
|
||||||
| Prefix | Use |
|
1. **Create a feature branch** from `dev`:
|
||||||
|--------|-----|
|
```bash
|
||||||
| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) |
|
git checkout dev && git pull
|
||||||
| `version/XX.YY` | Stable release (auto-created, never manually pushed) |
|
git checkout -b feature/my-change
|
||||||
| `chore/` | Automated sync branches (managed by MokoStandards) |
|
```
|
||||||
|
|
||||||
> **Never use** `feature/`, `hotfix/`, or `release/` prefixes — they are not part of the MokoStandards branch strategy.
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
|
|
||||||
## Commit Conventions
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
|
|
||||||
Use [conventional commits](https://www.conventionalcommits.org/):
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
|
- An RC pre-release is built and uploaded
|
||||||
|
|
||||||
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
|
- Stability suffix stripped (clean version)
|
||||||
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
|
- `updates.xml` updated (Joomla extensions)
|
||||||
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
|
### Branch summary
|
||||||
|
|
||||||
|
| Branch | Purpose | Created by |
|
||||||
|
|--------|---------|-----------|
|
||||||
|
| `feature/*` | New features and fixes | Developer |
|
||||||
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
|
| `main` | Stable releases | Protected, merge only |
|
||||||
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
|
### Protected branches
|
||||||
|
|
||||||
|
| Branch | Direct push | Merge via |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
|
## Version Policy
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
|
- **XX** — Major version (breaking changes)
|
||||||
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
|
### Stability suffixes
|
||||||
|
|
||||||
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
|
| Branch | Suffix | Example |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `main` | (none) | `02.09.00` |
|
||||||
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
|
### Auto version bump
|
||||||
|
|
||||||
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
|
1. Patch version incremented
|
||||||
|
2. Stability suffix `-dev` applied
|
||||||
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
|
### Release version flow
|
||||||
|
|
||||||
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
|
| Event | Bump | Example |
|
||||||
|
|-------|------|---------|
|
||||||
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
|
### Release stream copies
|
||||||
|
|
||||||
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
|
### Version files
|
||||||
|
|
||||||
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
|
- Joomla XML manifests (`<version>` tag)
|
||||||
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
|
- `package.json`, `pyproject.toml`
|
||||||
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Use conventional commit format:
|
||||||
|
|
||||||
```
|
```
|
||||||
feat(scope): add new extrafield for invoice tracking
|
type(scope): short description
|
||||||
fix(sql): correct column type in llx_mytable
|
|
||||||
docs(readme): update installation instructions
|
Optional body with context.
|
||||||
chore(deps): bump enterprise library to 04.02.30
|
|
||||||
|
Authored-by: Moko Consulting
|
||||||
```
|
```
|
||||||
|
|
||||||
**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build`
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
## Pull Request Workflow
|
Special flags in commit messages:
|
||||||
|
- `[skip ci]` — skip all CI workflows
|
||||||
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format
|
## Reporting Issues
|
||||||
2. **Bump** the patch version in `README.md` before opening the PR
|
|
||||||
3. **Title** must be a valid conventional commit subject line
|
|
||||||
4. **Target** `main` — squash merge only (merge commits are disabled)
|
|
||||||
5. **CI checks** must pass before merge
|
|
||||||
|
|
||||||
### What Happens on Merge
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
When your PR is merged to `main`, these workflows run automatically:
|
|
||||||
|
|
||||||
1. **sync-version-on-merge** — auto-bumps patch version, propagates to all file headers
|
|
||||||
2. **auto-release** — creates `version/XX.YY` branch, git tag, and GitHub Release
|
|
||||||
3. **deploy-demo / deploy-rs** — deploys to demo and RS servers (if `src/**` changed)
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards):
|
|
||||||
|
|
||||||
| Standard | Reference |
|
|
||||||
|----------|-----------|
|
|
||||||
| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) |
|
|
||||||
| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) |
|
|
||||||
| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) |
|
|
||||||
| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) |
|
|
||||||
| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) |
|
|
||||||
| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) |
|
|
||||||
|
|
||||||
## PR Checklist
|
|
||||||
|
|
||||||
- [ ] Branch named `dev/XX.YY.ZZ/description`
|
|
||||||
- [ ] Patch version bumped in `README.md`
|
|
||||||
- [ ] Conventional commit format for PR title
|
|
||||||
- [ ] All new files have FILE INFORMATION headers
|
|
||||||
- [ ] `declare(strict_types=1)` in all PHP files
|
|
||||||
- [ ] PHPDoc on all public methods
|
|
||||||
- [ ] Tests pass
|
|
||||||
- [ ] CHANGELOG.md updated
|
|
||||||
- [ ] No secrets, tokens, or credentials committed
|
|
||||||
|
|
||||||
## Custom Workflows
|
|
||||||
|
|
||||||
Place repo-specific workflows in `.github/workflows/custom/` — they are **never overwritten or deleted** by MokoStandards sync:
|
|
||||||
|
|
||||||
```
|
|
||||||
.github/workflows/
|
|
||||||
├── deploy-dev.yml ← Synced from MokoStandards
|
|
||||||
├── auto-release.yml ← Synced from MokoStandards
|
|
||||||
└── custom/ ← Your custom workflows (safe)
|
|
||||||
└── my-custom-ci.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly — changes will be overwritten on the next sync.*
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ PLUGIN_GROUP := system
|
|||||||
# Options: system, content, user, authentication, etc.
|
# Options: system, content, user, authentication, etc.
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
SRC_DIR := .
|
SRC_DIR := src
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
DIST_DIR := dist
|
DIST_DIR := dist
|
||||||
DOCS_DIR := docs
|
DOCS_DIR := docs
|
||||||
@@ -155,62 +155,12 @@ clean: ## Clean build artifacts
|
|||||||
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: clean validate ## Build extension package
|
build: clean ## Build extension package
|
||||||
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Building Joomla module extension...$(COLOR_RESET)"
|
||||||
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
|
@mkdir -p $(DIST_DIR)
|
||||||
|
@cd $(SRC_DIR) && $(ZIP) -r "$(CURDIR)/$(DIST_DIR)/mod_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" . \
|
||||||
# Determine package prefix based on extension type
|
-x "*.git*" -x "*/index.html" 2>/dev/null
|
||||||
@case "$(EXTENSION_TYPE)" in \
|
@echo "$(COLOR_GREEN)✓ Module created: $(DIST_DIR)/mod_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
||||||
module) \
|
|
||||||
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
plugin) \
|
|
||||||
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
component) \
|
|
||||||
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
package) \
|
|
||||||
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
template) \
|
|
||||||
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
*) \
|
|
||||||
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
|
|
||||||
exit 1; \
|
|
||||||
;; \
|
|
||||||
esac; \
|
|
||||||
\
|
|
||||||
mkdir -p "$$BUILD_TARGET"; \
|
|
||||||
\
|
|
||||||
echo "Building $$PACKAGE_PREFIX..."; \
|
|
||||||
\
|
|
||||||
rsync -av --progress \
|
|
||||||
--exclude='$(BUILD_DIR)' \
|
|
||||||
--exclude='$(DIST_DIR)' \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='vendor/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='tests/' \
|
|
||||||
--exclude='Makefile' \
|
|
||||||
--exclude='composer.json' \
|
|
||||||
--exclude='composer.lock' \
|
|
||||||
--exclude='package.json' \
|
|
||||||
--exclude='package-lock.json' \
|
|
||||||
--exclude='phpunit.xml' \
|
|
||||||
--exclude='*.md' \
|
|
||||||
--exclude='.editorconfig' \
|
|
||||||
. "$$BUILD_TARGET/"; \
|
|
||||||
\
|
|
||||||
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
|
|
||||||
\
|
|
||||||
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: package
|
.PHONY: package
|
||||||
package: build ## Alias for build
|
package: build ## Alias for build
|
||||||
@@ -325,49 +275,15 @@ security-check: ## Run security checks on dependencies
|
|||||||
$(NPM) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
$(NPM) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: deploy
|
.PHONY: minify
|
||||||
deploy: ## Deploy to a Joomla site via SSH (usage: make deploy HOST=user@host WEBROOT=/path/to/joomla)
|
minify: ## Minify CSS/JS assets
|
||||||
@if [ -z "$(HOST)" ] || [ -z "$(WEBROOT)" ]; then \
|
@echo "Minifying assets..."
|
||||||
echo "$(COLOR_RED)✗ Usage: make deploy HOST=user@host WEBROOT=/path/to/joomla [KEY=~/.ssh/id_rsa]$(COLOR_RESET)"; \
|
@MOKO_PLATFORM=$$(echo ../moko-platform $$HOME/moko-platform /opt/moko-platform | tr ' ' '\n' | while read p; do [ -d "$$p" ] && echo "$$p" && break; done); \
|
||||||
exit 1; \
|
if [ -n "$$MOKO_PLATFORM" ] && [ -f "$$MOKO_PLATFORM/build/minify.js" ]; then \
|
||||||
|
node "$$MOKO_PLATFORM/build/minify.js" $(SRC_DIR); \
|
||||||
|
else \
|
||||||
|
echo "No minify script found"; \
|
||||||
fi
|
fi
|
||||||
@SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"; \
|
|
||||||
if [ -n "$(KEY)" ]; then SSH_OPTS="$$SSH_OPTS -i $(KEY)"; fi; \
|
|
||||||
echo "$(COLOR_BLUE)Deploying mod_$(EXTENSION_NAME) to $(HOST):$(WEBROOT)...$(COLOR_RESET)"; \
|
|
||||||
ssh $$SSH_OPTS $(HOST) "\
|
|
||||||
W=$(WEBROOT) && \
|
|
||||||
cp -r \$$W/modules/mod_$(EXTENSION_NAME)/language/en-US/* /dev/null 2>&1; \
|
|
||||||
true" && \
|
|
||||||
for f in src/mod_mokojoomhero.php src/mod_mokojoomhero.xml src/script.php; do \
|
|
||||||
scp $$SSH_OPTS $$f $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/$$(basename $$f); \
|
|
||||||
done && \
|
|
||||||
scp -r $$SSH_OPTS src/tmpl/* $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/tmpl/ && \
|
|
||||||
scp -r $$SSH_OPTS src/language/* $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/language/ && \
|
|
||||||
scp $$SSH_OPTS src/media/joomla.asset.json $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/ && \
|
|
||||||
scp -r $$SSH_OPTS src/media/css/* $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/css/ && \
|
|
||||||
scp -r $$SSH_OPTS src/media/js/* $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/js/ && \
|
|
||||||
ssh $$SSH_OPTS $(HOST) "\
|
|
||||||
W=$(WEBROOT) && \
|
|
||||||
mkdir -p \$$W/images/heroes && \
|
|
||||||
for lang in en-US en-GB; do \
|
|
||||||
for ini in mod_mokojoomhero.ini mod_mokojoomhero.sys.ini; do \
|
|
||||||
src=\$$W/modules/mod_$(EXTENSION_NAME)/language/\$$lang/\$$ini; \
|
|
||||||
if [ -f \$$src ]; then \
|
|
||||||
cp \$$src \$$W/administrator/language/\$$lang/\$$ini 2>/dev/null; \
|
|
||||||
cp \$$src \$$W/language/\$$lang/\$$ini 2>/dev/null; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
done && \
|
|
||||||
echo 'OK'" && \
|
|
||||||
echo "$(COLOR_GREEN)✓ Deployed to $(HOST)$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: deploy-all
|
|
||||||
deploy-all: ## Deploy to all configured sites (requires SITES_FILE or inline)
|
|
||||||
@echo "$(COLOR_BLUE)Deploying to all sites...$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_YELLOW)Usage: Create a sites.conf with HOST:WEBROOT per line, then:$(COLOR_RESET)"
|
|
||||||
@echo " while IFS=: read -r host webroot; do"
|
|
||||||
@echo " make deploy HOST=\$$host WEBROOT=\$$webroot KEY=path/to/key"
|
|
||||||
@echo " done < sites.conf"
|
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: install-deps validate test build ## Run complete build pipeline
|
all: install-deps validate test build ## Run complete build pipeline
|
||||||
|
|||||||
@@ -7,108 +7,83 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
FILE: ./README.md
|
FILE: ./README.md
|
||||||
VERSION: 01.07.00
|
BRIEF: MokoJoomHero - Joomla Hero Module
|
||||||
BRIEF: MokoJoomHero - Joomla Module
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoJoomHero
|
# MokoJoomHero
|
||||||
|
|
||||||
A Joomla 5 site template developed following MokoStandards.
|
A Joomla site module for random hero image slideshows, video backgrounds, solid colours, gradients, and content overlays.
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
MokoJoomHero is a modern, accessible Joomla 5 template by Moko Consulting. It features a prominent hero section, flexible module positions, sticky header, customisable brand colour, and responsive layout — all built on Joomla's Web Asset Manager.
|
MokoJoomHero is a free, open-source Joomla module that creates dynamic hero sections with multiple background modes, configurable content overlays, and advanced features like parallax scrolling, A/B testing, and scheduling. Designed for the MokoOnyx template but works with any Joomla 5/6 template.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Hero Section**: Dedicated `hero` module position with gradient styling
|
### Background Modes
|
||||||
- **Flexible Layout**: 11 module positions including sidebars, topbar, banner, and footer
|
- **Image Slideshow** — random images from a folder with 4 transition types (crossfade, slide, fade-to-black, zoom/Ken Burns)
|
||||||
- **Sticky Header**: Optional sticky navigation with smooth scroll
|
- **Video** — YouTube, Vimeo, or local video with autoplay, mute toggle, and poster image
|
||||||
- **Brand Customisation**: Configurable brand colour, logo, and tagline via Template Manager
|
- **Solid Colour** — single colour picker
|
||||||
- **Responsive**: Mobile-first layout with CSS custom properties
|
- **Gradient** — two-colour gradient with configurable angle
|
||||||
- **Accessibility**: Semantic HTML5, ARIA landmarks, skip-to-content support
|
|
||||||
- **Web Asset Manager**: Modern asset loading via `joomla.asset.json`
|
### Content & Overlay
|
||||||
- **Error & Offline Pages**: Custom styled error and offline templates
|
- **WYSIWYG Editor** or **Joomla Article** as content source
|
||||||
|
- **Per-slide content** — unique heading, body, and CTA per image slide via subform
|
||||||
|
- **Card mode** — white card with shadow and fade-in delay
|
||||||
|
- **Overlay** — solid or directional gradient (dark at bottom/top/left/right)
|
||||||
|
- **Text alignment** — horizontal (left/center/right) and vertical (top/center/bottom)
|
||||||
|
- **Content animations** — fade-in, slide-up, slide-left, slide-right with configurable delay
|
||||||
|
|
||||||
|
### Advanced
|
||||||
|
- **Parallax scroll** — background moves at configurable speed (0.1–0.9)
|
||||||
|
- **A/B testing** — weighted random content variations, session-sticky per visitor
|
||||||
|
- **Scheduling** — start/end datetime with site timezone support
|
||||||
|
- **Scroll indicator** — animated chevron with smooth-scroll click handler
|
||||||
|
- **Mobile height** — separate height setting for mobile viewports
|
||||||
|
- **Reduced motion** — WCAG 2.1 AA compliant, respects `prefers-reduced-motion`
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **PHP**: 8.1 or higher
|
- **PHP**: 8.1 or higher
|
||||||
- **Joomla**: 5.x (also compatible with 6.x)
|
- **Joomla**: 5.x or 6.x
|
||||||
- **Make**: GNU Make for build automation
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Build the template package:
|
1. Download the latest release from the [releases page](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases)
|
||||||
```bash
|
2. Upload `mod_mokojoomhero-*.zip` via **System > Install > Upload Package File**
|
||||||
make build
|
3. Assign the module to a template position and configure
|
||||||
```
|
|
||||||
|
|
||||||
2. Upload the generated `tpl_mokojoomhero-1.0.0.zip` via Joomla's Extension Manager
|
Or build from source:
|
||||||
|
|
||||||
3. Set as default template in **System → Site Template Styles**
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Display available commands
|
|
||||||
make help
|
|
||||||
|
|
||||||
# Validate code
|
|
||||||
make validate
|
|
||||||
|
|
||||||
# Build template package
|
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Module Positions
|
## Usage
|
||||||
|
|
||||||
| Position | Purpose |
|
1. Go to **Content > Site Modules > MokoJoomHero**
|
||||||
|----------|---------|
|
2. Select a **Hero Mode** (Images, Video, Solid Colour, or Gradient)
|
||||||
| `topbar` | Slim bar above the header (contact info, social links) |
|
3. Add content via the **Hero Content** tab
|
||||||
| `banner` | Full-width banner below the header |
|
4. Configure overlay, text alignment, and animations in **Overlay & Text**
|
||||||
| `menu` | Main navigation inside the header |
|
5. Assign to a module position and publish
|
||||||
| `hero` | Hero section with gradient background |
|
|
||||||
| `breadcrumbs` | Breadcrumb trail |
|
|
||||||
| `sidebar-left` | Left sidebar |
|
|
||||||
| `sidebar-right` | Right sidebar |
|
|
||||||
| `main-top` | Above the main content area |
|
|
||||||
| `main-bottom` | Below the main content area |
|
|
||||||
| `footer` | Footer area |
|
|
||||||
| `debug` | Debug output (hidden from users) |
|
|
||||||
|
|
||||||
## Template Parameters
|
|
||||||
|
|
||||||
Configure via **System → Site Template Styles → MokoJoomHero**:
|
|
||||||
|
|
||||||
- **Logo** — Upload a site logo image
|
|
||||||
- **Site Description** — Tagline displayed next to the logo
|
|
||||||
- **Brand Colour** — Primary accent colour (default: `#1a73e8`)
|
|
||||||
- **Fluid Container** — Toggle full-width vs fixed-width layout
|
|
||||||
- **Sticky Header** — Keep the header visible on scroll
|
|
||||||
- **Back to Top** — Floating scroll-to-top button
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
src/
|
||||||
├── docs/ # Documentation files
|
├── mod_mokojoomhero.php # Module entry point
|
||||||
├── scripts/ # Build and deployment scripts
|
├── mod_mokojoomhero.xml # Joomla manifest
|
||||||
├── src/ # Template source code
|
├── script.php # Install script
|
||||||
│ ├── css/template.css # Main stylesheet
|
├── tmpl/default.php # Template
|
||||||
│ ├── js/template.js # Main JavaScript
|
├── media/
|
||||||
│ ├── images/ # Template images
|
│ ├── css/mod_mokojoomhero.css
|
||||||
│ ├── html/ # Template overrides
|
│ ├── js/mod_mokojoomhero.js
|
||||||
│ ├── language/en-GB/ # Language files
|
│ └── joomla.asset.json
|
||||||
│ ├── templateDetails.xml # Joomla manifest
|
└── language/
|
||||||
│ ├── joomla.asset.json # Web Asset Manager config
|
├── en-GB/
|
||||||
│ ├── index.php # Main template
|
└── en-US/
|
||||||
│ ├── error.php # Error page
|
|
||||||
│ ├── offline.php # Offline page
|
|
||||||
│ └── component.php # Component-only output
|
|
||||||
├── Makefile # Build automation
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -117,9 +92,7 @@ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guid
|
|||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
This project uses [Semantic Versioning](https://semver.org/).
|
This project uses [Semantic Versioning](https://semver.org/). See [CHANGELOG.md](CHANGELOG.md) for version history.
|
||||||
|
|
||||||
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -131,6 +104,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- **Documentation**: See the [docs/](docs/) directory
|
- **Wiki**: [MokoJoomHero Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/wiki)
|
||||||
|
- **Issues**: [Issue Tracker](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/issues)
|
||||||
- **Contact**: hello@mokoconsulting.tech
|
- **Contact**: hello@mokoconsulting.tech
|
||||||
- **Website**: [mokoconsulting.tech](https://mokoconsulting.tech)
|
- **Website**: [mokoconsulting.tech](https://mokoconsulting.tech)
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
|||||||
INGROUP: [PROJECT_NAME].Documentation
|
INGROUP: [PROJECT_NAME].Documentation
|
||||||
REPO: [REPOSITORY_URL]
|
REPO: [REPOSITORY_URL]
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 01.07.00
|
VERSION: 01.19.00
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Automation.CI
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /automation/ci-issue-reporter.sh
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||||
|
# Deduplicates by searching open issues with the "ci-auto" label
|
||||||
|
# whose title matches the gate. If a matching issue exists, a comment
|
||||||
|
# is appended instead of opening a duplicate.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||||
|
LABEL_NAME="ci-auto"
|
||||||
|
LABEL_COLOR="#e11d48"
|
||||||
|
|
||||||
|
GATE=""
|
||||||
|
DETAILS=""
|
||||||
|
SEVERITY="error"
|
||||||
|
WORKFLOW=""
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||||
|
--details Human-readable failure description
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--severity "error" (default) or "warning"
|
||||||
|
--workflow Workflow name for the issue title
|
||||||
|
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||||
|
--run-url URL to the CI run (auto-detected from env)
|
||||||
|
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||||
|
--url Gitea base URL (default: \$GITEA_URL)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gate) GATE="$2"; shift 2 ;;
|
||||||
|
--details) DETAILS="$2"; shift 2 ;;
|
||||||
|
--severity) SEVERITY="$2"; shift 2 ;;
|
||||||
|
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||||
|
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||||
|
--url) GITEA_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||||
|
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||||
|
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||||
|
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||||
|
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||||
|
|
||||||
|
# ── Build title ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -n "$WORKFLOW" ]]; then
|
||||||
|
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||||
|
else
|
||||||
|
TITLE="[CI] ${GATE} failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||||
|
ensure_label() {
|
||||||
|
local exists
|
||||||
|
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$exists" == "200" ]]; then
|
||||||
|
# Check if label already exists
|
||||||
|
local found
|
||||||
|
found=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||||
|
|
||||||
|
if [[ -z "$found" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/labels" \
|
||||||
|
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Search for existing open issue ──────────────────────────────────────────
|
||||||
|
find_existing_issue() {
|
||||||
|
# URL-encode the gate name for the query
|
||||||
|
local query
|
||||||
|
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||||
|
2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Extract the first matching issue number
|
||||||
|
echo "$response" \
|
||||||
|
| grep -oP '"number":\s*\K[0-9]+' \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build issue body ────────────────────────────────────────────────────────
|
||||||
|
build_body() {
|
||||||
|
local severity_badge
|
||||||
|
if [[ "$SEVERITY" == "error" ]]; then
|
||||||
|
severity_badge="**Severity:** Error"
|
||||||
|
else
|
||||||
|
severity_badge="**Severity:** Warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<BODY
|
||||||
|
## CI Gate Failure: ${GATE}
|
||||||
|
|
||||||
|
${severity_badge}
|
||||||
|
**Workflow:** ${WORKFLOW:-unknown}
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||||
|
BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||||
|
build_comment() {
|
||||||
|
cat <<COMMENT
|
||||||
|
### CI failure recurrence
|
||||||
|
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_label
|
||||||
|
|
||||||
|
EXISTING=$(find_existing_issue)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
# Append comment to existing issue
|
||||||
|
COMMENT_BODY=$(build_comment)
|
||||||
|
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||||
|
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${EXISTING}/comments" \
|
||||||
|
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$HTTP" == "201" ]]; then
|
||||||
|
echo "Commented on existing issue #${EXISTING}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new issue
|
||||||
|
ISSUE_BODY=$(build_body)
|
||||||
|
ISSUE_JSON=$(python3 -c "
|
||||||
|
import sys, json
|
||||||
|
body = sys.stdin.read()
|
||||||
|
print(json.dumps({
|
||||||
|
'title': sys.argv[1],
|
||||||
|
'body': body,
|
||||||
|
'labels': []
|
||||||
|
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues" \
|
||||||
|
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ISSUE_NUM" ]]; then
|
||||||
|
# Apply label (separate call — more reliable across Gitea versions)
|
||||||
|
LABEL_ID=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||||
|
| head -1 || true)
|
||||||
|
|
||||||
|
if [[ -n "$LABEL_ID" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||||
|
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create issue"
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Docs Index: /templates/repos/joomla/module/src
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This index provides navigation to documentation within this folder.
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
|
|
||||||
- **Document Type:** index
|
|
||||||
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
|
|
||||||
|
|
||||||
## Revision History
|
|
||||||
|
|
||||||
| Change | Notes | Author |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Automated update | Generated by documentation index automation | rebuild_indexes.py |
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -6,24 +6,43 @@
|
|||||||
; INGROUP: MokoJoomHero.Module
|
; INGROUP: MokoJoomHero.Module
|
||||||
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
; PATH: /src/language/en-GB/mod_mokojoomhero.ini
|
; PATH: /src/language/en-GB/mod_mokojoomhero.ini
|
||||||
; VERSION: 01.07.00
|
; VERSION: 01.19.00
|
||||||
; BRIEF: Language strings for MokoJoomHero module (frontend + admin form fields)
|
; BRIEF: Language strings for MokoJoomHero module (frontend + admin form fields)
|
||||||
|
|
||||||
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
|
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
|
||||||
|
|
||||||
; Content fieldset
|
; Content fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content. The article introtext (or fulltext) is displayed."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
||||||
|
|
||||||
; Hero mode
|
; Hero mode
|
||||||
MOD_MOKOJOOMHERO_MODE_LABEL="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_IMAGES="Images"
|
||||||
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
||||||
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
|
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
|
; Image settings
|
||||||
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
||||||
@@ -33,6 +52,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
|
|||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
||||||
|
|
||||||
|
; Per-slide content
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content. When populated, this overrides the random image folder. Leave empty to use the folder-based slideshow."
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
|
||||||
|
|
||||||
; Video settings (embedded)
|
; Video settings (embedded)
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
||||||
@@ -41,31 +69,104 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
|
|||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
||||||
|
|
||||||
|
; Content animation
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_NONE="None"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
|
||||||
|
|
||||||
; Card delay
|
; Card delay
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
||||||
|
|
||||||
|
; Parallax
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll, creating a depth effect."
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
|
||||||
|
|
||||||
|
; A/B testing
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors. Assignment is sticky per session."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights. Higher weight = higher chance of being shown."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
|
||||||
|
|
||||||
|
; Scheduling
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range. Uses the site timezone."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time. Leave empty for no start restriction."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time. Leave empty for no end restriction."
|
||||||
|
|
||||||
|
; Video poster
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections."
|
||||||
|
|
||||||
|
; Scroll indicator
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
|
||||||
|
|
||||||
; Mute toggle
|
; Mute toggle
|
||||||
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
|
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)."
|
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
|
; Hero height
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="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)."
|
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)."
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
||||||
|
|
||||||
|
; Hero height (mobile)
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height."
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
|
||||||
|
|
||||||
; Overlay fieldset
|
; Overlay fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
|
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
|
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
|
||||||
|
|
||||||
; Alignment options
|
; Horizontal alignment options
|
||||||
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
|
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
; INGROUP: MokoJoomHero.Module
|
; INGROUP: MokoJoomHero.Module
|
||||||
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
; PATH: /src/language/en-GB/mod_mokojoomhero.sys.ini
|
; PATH: /src/language/en-GB/mod_mokojoomhero.sys.ini
|
||||||
; VERSION: 01.07.00
|
; VERSION: 01.19.00
|
||||||
; BRIEF: System language strings — used in admin Extension Manager and Module Manager
|
; BRIEF: System language strings — used in admin Extension Manager and Module Manager
|
||||||
|
|
||||||
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
|
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
|
||||||
@@ -14,17 +14,36 @@ MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or backgrou
|
|||||||
|
|
||||||
; Content fieldset
|
; Content fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
||||||
|
|
||||||
; Hero mode
|
; Hero mode
|
||||||
MOD_MOKOJOOMHERO_MODE_LABEL="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_IMAGES="Images"
|
||||||
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
||||||
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
|
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
|
; Image settings
|
||||||
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
||||||
@@ -34,6 +53,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
|
|||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
||||||
|
|
||||||
|
; Per-slide content
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content."
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
|
||||||
|
|
||||||
; Video settings (embedded)
|
; Video settings (embedded)
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
||||||
@@ -42,31 +70,104 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
|
|||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
||||||
|
|
||||||
|
; Content animation
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_NONE="None"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
|
||||||
|
|
||||||
; Card delay
|
; Card delay
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
||||||
|
|
||||||
|
; Parallax
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll."
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
|
||||||
|
|
||||||
|
; A/B testing
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
|
||||||
|
|
||||||
|
; Scheduling
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time."
|
||||||
|
|
||||||
|
; Video poster
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads."
|
||||||
|
|
||||||
|
; Scroll indicator
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
|
||||||
|
|
||||||
; Mute toggle
|
; Mute toggle
|
||||||
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
|
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)."
|
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
|
; Hero height
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="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)."
|
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)."
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
||||||
|
|
||||||
|
; Hero height (mobile)
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height."
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
|
||||||
|
|
||||||
; Overlay fieldset
|
; Overlay fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
|
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
|
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
|
||||||
|
|
||||||
; Alignment options
|
; Horizontal alignment options
|
||||||
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
|
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -6,24 +6,43 @@
|
|||||||
; INGROUP: MokoJoomHero.Module
|
; INGROUP: MokoJoomHero.Module
|
||||||
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
; PATH: /src/language/en-US/mod_mokojoomhero.ini
|
; PATH: /src/language/en-US/mod_mokojoomhero.ini
|
||||||
; VERSION: 01.07.00
|
; VERSION: 01.19.00
|
||||||
; BRIEF: Language strings for MokoJoomHero module (en-US, frontend + admin form fields)
|
; BRIEF: Language strings for MokoJoomHero module (en-US, frontend + admin form fields)
|
||||||
|
|
||||||
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
|
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
|
||||||
|
|
||||||
; Content fieldset
|
; Content fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content. The article introtext (or fulltext) is displayed."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
||||||
|
|
||||||
; Hero mode
|
; Hero mode
|
||||||
MOD_MOKOJOOMHERO_MODE_LABEL="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_IMAGES="Images"
|
||||||
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
||||||
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
|
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
|
; Image settings
|
||||||
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
||||||
@@ -33,6 +52,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
|
|||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
||||||
|
|
||||||
|
; Per-slide content
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content. When populated, this overrides the random image folder. Leave empty to use the folder-based slideshow."
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
|
||||||
|
|
||||||
; Video settings (embedded)
|
; Video settings (embedded)
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
||||||
@@ -41,6 +69,18 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
|
|||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
||||||
|
|
||||||
|
; 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
|
; Hero height
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="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)."
|
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)."
|
||||||
@@ -50,22 +90,83 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
|||||||
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
||||||
|
|
||||||
|
; Content animation
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_NONE="None"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
|
||||||
|
|
||||||
|
; Parallax
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll, creating a depth effect."
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
|
||||||
|
|
||||||
|
; A/B testing
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors. Assignment is sticky per session."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights. Higher weight = higher chance of being shown."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
|
||||||
|
|
||||||
|
; Scheduling
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range. Uses the site timezone."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time. Leave empty for no start restriction."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time. Leave empty for no end restriction."
|
||||||
|
|
||||||
|
; Video poster
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections."
|
||||||
|
|
||||||
|
; Scroll indicator
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
|
||||||
|
|
||||||
; Mute toggle
|
; Mute toggle
|
||||||
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
|
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)."
|
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
|
||||||
|
|
||||||
|
; Hero height (mobile)
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height."
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
|
||||||
|
|
||||||
; Overlay fieldset
|
; Overlay fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_CENTER="Center"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
|
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
|
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
|
||||||
|
|
||||||
; Alignment options
|
; Horizontal alignment options
|
||||||
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
|
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
; INGROUP: MokoJoomHero.Module
|
; INGROUP: MokoJoomHero.Module
|
||||||
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
; PATH: /src/language/en-US/mod_mokojoomhero.sys.ini
|
; PATH: /src/language/en-US/mod_mokojoomhero.sys.ini
|
||||||
; VERSION: 01.07.00
|
; VERSION: 01.19.00
|
||||||
; BRIEF: System language strings — used in admin Extension Manager and Module Manager (en-US)
|
; BRIEF: System language strings — used in admin Extension Manager and Module Manager (en-US)
|
||||||
|
|
||||||
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
|
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
|
||||||
@@ -14,17 +14,36 @@ MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or backgrou
|
|||||||
|
|
||||||
; Content fieldset
|
; Content fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
|
||||||
|
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
|
||||||
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content."
|
||||||
|
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
|
||||||
|
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
|
||||||
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
|
||||||
|
|
||||||
; Hero mode
|
; Hero mode
|
||||||
MOD_MOKOJOOMHERO_MODE_LABEL="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_IMAGES="Images"
|
||||||
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
|
||||||
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
|
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
|
; Image settings
|
||||||
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
|
||||||
@@ -34,6 +53,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
|
|||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
|
||||||
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
|
||||||
|
|
||||||
|
; Per-slide content
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content."
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
|
||||||
|
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
|
||||||
|
|
||||||
; Video settings (embedded)
|
; Video settings (embedded)
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
|
||||||
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
|
||||||
@@ -42,31 +70,104 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
|
|||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
|
||||||
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
|
||||||
|
|
||||||
|
; Content animation
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
|
||||||
|
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_NONE="None"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
|
||||||
|
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
|
||||||
|
|
||||||
; Card delay
|
; Card delay
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
|
||||||
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
|
||||||
|
|
||||||
|
; Parallax
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll."
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
|
||||||
|
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
|
||||||
|
|
||||||
|
; A/B testing
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
|
||||||
|
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights."
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
|
||||||
|
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
|
||||||
|
|
||||||
|
; Scheduling
|
||||||
|
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time."
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
|
||||||
|
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time."
|
||||||
|
|
||||||
|
; Video poster
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
|
||||||
|
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads."
|
||||||
|
|
||||||
|
; Scroll indicator
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
|
||||||
|
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
|
||||||
|
|
||||||
; Mute toggle
|
; Mute toggle
|
||||||
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
|
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)."
|
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
|
; Hero height
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="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)."
|
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)."
|
||||||
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
|
||||||
|
|
||||||
|
; Hero height (mobile)
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height."
|
||||||
|
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
|
||||||
|
|
||||||
; Overlay fieldset
|
; Overlay fieldset
|
||||||
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay & Text"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
|
||||||
|
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
|
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
|
||||||
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
|
||||||
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_CENTER="Center"
|
||||||
|
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
|
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
|
||||||
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
|
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
|
||||||
|
|
||||||
; Alignment options
|
; Horizontal alignment options
|
||||||
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
|
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
|
||||||
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
* DEFGROUP: MokoJoomHero.Module.Assets
|
* DEFGROUP: MokoJoomHero.Module.Assets
|
||||||
* INGROUP: MokoJoomHero.Module
|
* INGROUP: MokoJoomHero.Module
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
* PATH: /src/css/template.css
|
* PATH: /src/media/css/mod_mokojoomhero.css
|
||||||
* VERSION: 01.07.00
|
* VERSION: 01.19.00
|
||||||
* BRIEF: Hero module stylesheet — slideshow, video background, overlay
|
* BRIEF: Hero module stylesheet — slideshow, video, colour/gradient, overlay, card, mute toggle, responsive
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
@@ -23,7 +23,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Image slides
|
Solid colour / gradient background
|
||||||
|
============================================================ */
|
||||||
|
.mokojoomhero__color {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Image slides — base
|
||||||
============================================================ */
|
============================================================ */
|
||||||
.mokojoomhero__slide {
|
.mokojoomhero__slide {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -32,13 +40,51 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 1s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mokojoomhero__slide--active {
|
.mokojoomhero__slide--active {
|
||||||
opacity: 1;
|
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
|
Video background
|
||||||
============================================================ */
|
============================================================ */
|
||||||
@@ -75,7 +121,6 @@ iframe.mokojoomhero__video {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -165,12 +210,147 @@ iframe.mokojoomhero__video {
|
|||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Video poster image
|
||||||
|
============================================================ */
|
||||||
|
.mokojoomhero__poster {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Scroll-down indicator
|
||||||
|
============================================================ */
|
||||||
|
.mokojoomhero__scroll-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
animation: mokojoomhero-bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__scroll-indicator:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__scroll-indicator--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mokojoomhero-bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% { transform: translateX(-50%) translateY(0); }
|
||||||
|
40% { transform: translateX(-50%) translateY(-8px); }
|
||||||
|
60% { transform: translateX(-50%) translateY(-4px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Content entrance animations
|
||||||
|
============================================================ */
|
||||||
|
.mokojoomhero__content[class*="mokojoomhero__content--anim-"] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__content--anim-fade-in.mokojoomhero__content--visible {
|
||||||
|
animation: mokojoomhero-anim-fade-in 0.8s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__content--anim-slide-up.mokojoomhero__content--visible {
|
||||||
|
animation: mokojoomhero-anim-slide-up 0.8s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__content--anim-slide-left.mokojoomhero__content--visible {
|
||||||
|
animation: mokojoomhero-anim-slide-left 0.8s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__content--anim-slide-right.mokojoomhero__content--visible {
|
||||||
|
animation: mokojoomhero-anim-slide-right 0.8s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mokojoomhero-anim-fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mokojoomhero-anim-slide-up {
|
||||||
|
from { opacity: 0; transform: translateY(30px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mokojoomhero-anim-slide-left {
|
||||||
|
from { opacity: 0; transform: translateX(30px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mokojoomhero-anim-slide-right {
|
||||||
|
from { opacity: 0; transform: translateX(-30px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Parallax
|
||||||
|
============================================================ */
|
||||||
|
.mokojoomhero[data-parallax] .mokojoomhero__slide,
|
||||||
|
.mokojoomhero[data-parallax] .mokojoomhero__color,
|
||||||
|
.mokojoomhero[data-parallax] .mokojoomhero__poster,
|
||||||
|
.mokojoomhero[data-parallax] video.mokojoomhero__video,
|
||||||
|
.mokojoomhero[data-parallax] iframe.mokojoomhero__video {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Reduced motion — WCAG 2.1 AA (SC 2.3.3)
|
||||||
|
============================================================ */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mokojoomhero__slide {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__card[data-card-delay] {
|
||||||
|
opacity: 1;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__scroll-indicator {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero__content[class*="mokojoomhero__content--anim-"] {
|
||||||
|
opacity: 1;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokojoomhero[data-parallax] .mokojoomhero__slide,
|
||||||
|
.mokojoomhero[data-parallax] .mokojoomhero__color,
|
||||||
|
.mokojoomhero[data-parallax] .mokojoomhero__poster,
|
||||||
|
.mokojoomhero[data-parallax] video.mokojoomhero__video,
|
||||||
|
.mokojoomhero[data-parallax] iframe.mokojoomhero__video {
|
||||||
|
will-change: auto;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Responsive
|
Responsive
|
||||||
============================================================ */
|
============================================================ */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mokojoomhero {
|
.mokojoomhero {
|
||||||
height: auto !important;
|
height: var(--mokojoomhero-mobile-height, auto) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mokojoomhero__video,
|
.mokojoomhero__video,
|
||||||
@@ -178,8 +358,17 @@ iframe.mokojoomhero__video {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep colour/gradient backgrounds visible on mobile */
|
||||||
|
.mokojoomhero__color {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.mokojoomhero__overlay {
|
.mokojoomhero__overlay {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only clear overlay background when media is hidden (image/video modes) */
|
||||||
|
.mokojoomhero:not(:has(.mokojoomhero__color)) .mokojoomhero__overlay {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body bgcolor="#FFFFFF"></body></html>
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
* DEFGROUP: MokoJoomHero.Module.Assets
|
* DEFGROUP: MokoJoomHero.Module.Assets
|
||||||
* INGROUP: MokoJoomHero.Module
|
* INGROUP: MokoJoomHero.Module
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
|
||||||
* PATH: /src/js/template.js
|
* PATH: /src/media/js/mod_mokojoomhero.js
|
||||||
* VERSION: 01.07.00
|
* VERSION: 01.19.00
|
||||||
* BRIEF: Hero module JavaScript — image slideshow crossfade
|
* BRIEF: Hero module JavaScript — slideshow crossfade, video viewport control, mute toggle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -20,25 +20,114 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
// ── Image slideshow ──
|
// ── Image slideshow ──
|
||||||
document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) {
|
document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) {
|
||||||
var slides = hero.querySelectorAll('.mokojoomhero__slide');
|
var slides = hero.querySelectorAll('.mokojoomhero__slide');
|
||||||
var interval = parseInt(hero.dataset.interval, 10) || 5000;
|
var interval = parseInt(hero.dataset.interval, 10) || 5000;
|
||||||
var current = 0;
|
var transition = hero.dataset.transition || 'crossfade';
|
||||||
|
var current = 0;
|
||||||
|
|
||||||
if (slides.length < 2) {
|
// Per-slide content data
|
||||||
|
var slideContentData = null;
|
||||||
|
var contentEl = hero.querySelector('.mokojoomhero__content');
|
||||||
|
|
||||||
|
if (hero.dataset.slideContent) {
|
||||||
|
try {
|
||||||
|
slideContentData = JSON.parse(hero.dataset.slideContent);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('MokoJoomHero: Failed to parse slide content data:', e.message);
|
||||||
|
slideContentData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slides.length < 2 || prefersReducedMotion) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(function () {
|
function updateSlideContent(index) {
|
||||||
slides[current].classList.remove('mokojoomhero__slide--active');
|
if (!slideContentData || !slideContentData[index] || !contentEl) {
|
||||||
slides[current].setAttribute('aria-hidden', 'true');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = slideContentData[index];
|
||||||
|
var card = contentEl.querySelector('.mokojoomhero__card');
|
||||||
|
var target = card || contentEl;
|
||||||
|
|
||||||
|
// Clear existing content safely
|
||||||
|
while (target.firstChild) {
|
||||||
|
target.removeChild(target.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.heading) {
|
||||||
|
var h2 = document.createElement('h2');
|
||||||
|
h2.className = 'mokojoomhero__title';
|
||||||
|
h2.textContent = data.heading;
|
||||||
|
target.appendChild(h2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.body) {
|
||||||
|
var p = document.createElement('p');
|
||||||
|
p.textContent = data.body;
|
||||||
|
target.appendChild(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.link && data.linkText) {
|
||||||
|
var linkP = document.createElement('p');
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = data.link;
|
||||||
|
a.className = 'btn btn-primary';
|
||||||
|
a.textContent = data.linkText;
|
||||||
|
linkP.appendChild(a);
|
||||||
|
target.appendChild(linkP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceSlide() {
|
||||||
|
var prev = current;
|
||||||
current = (current + 1) % slides.length;
|
current = (current + 1) % slides.length;
|
||||||
|
|
||||||
slides[current].classList.add('mokojoomhero__slide--active');
|
if (transition === 'slide') {
|
||||||
slides[current].setAttribute('aria-hidden', 'false');
|
slides[prev].classList.add('mokojoomhero__slide--exit');
|
||||||
}, interval);
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSlideContent(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial slide content
|
||||||
|
updateSlideContent(0);
|
||||||
|
|
||||||
|
setInterval(advanceSlide, interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Pause/resume videos when out of viewport ──
|
// ── Pause/resume videos when out of viewport ──
|
||||||
@@ -55,9 +144,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
// Resume
|
// Resume
|
||||||
if (video) {
|
if (video) {
|
||||||
video.play();
|
var playPromise = video.play();
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise.catch(function () {
|
||||||
|
// Autoplay blocked by browser policy — not actionable
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (iframe) {
|
if (iframe && iframe.contentWindow) {
|
||||||
var src = iframe.src || '';
|
var src = iframe.src || '';
|
||||||
if (src.indexOf('youtube') !== -1) {
|
if (src.indexOf('youtube') !== -1) {
|
||||||
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
|
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
|
||||||
@@ -70,7 +164,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (video) {
|
if (video) {
|
||||||
video.pause();
|
video.pause();
|
||||||
}
|
}
|
||||||
if (iframe) {
|
if (iframe && iframe.contentWindow) {
|
||||||
var src = iframe.src || '';
|
var src = iframe.src || '';
|
||||||
if (src.indexOf('youtube') !== -1) {
|
if (src.indexOf('youtube') !== -1) {
|
||||||
iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
||||||
@@ -86,9 +180,92 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
observer.observe(hero);
|
observer.observe(hero);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Content entrance animations ──
|
||||||
|
if (!prefersReducedMotion) {
|
||||||
|
var animObserver = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('mokojoomhero__content--visible');
|
||||||
|
animObserver.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.2 });
|
||||||
|
|
||||||
|
document.querySelectorAll('.mokojoomhero__content[class*="mokojoomhero__content--anim-"]').forEach(function (el) {
|
||||||
|
animObserver.observe(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Parallax scroll ──
|
||||||
|
if (!prefersReducedMotion) {
|
||||||
|
var parallaxHeroes = document.querySelectorAll('.mokojoomhero[data-parallax]');
|
||||||
|
|
||||||
|
if (parallaxHeroes.length) {
|
||||||
|
var onScroll = function () {
|
||||||
|
parallaxHeroes.forEach(function (hero) {
|
||||||
|
var rect = hero.getBoundingClientRect();
|
||||||
|
var speed = parseFloat(hero.dataset.parallax) || 0.5;
|
||||||
|
|
||||||
|
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
||||||
|
var offset = Math.round(rect.top * speed * -1);
|
||||||
|
var bg = hero.querySelector('.mokojoomhero__slide, .mokojoomhero__color, .mokojoomhero__poster, video.mokojoomhero__video');
|
||||||
|
|
||||||
|
if (bg) {
|
||||||
|
bg.style.transform = 'translateY(' + offset + 'px)';
|
||||||
|
}
|
||||||
|
|
||||||
|
var iframe = hero.querySelector('iframe.mokojoomhero__video');
|
||||||
|
|
||||||
|
if (iframe) {
|
||||||
|
iframe.style.transform = 'translate(-50%, calc(-50% + ' + offset + 'px))';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
onScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Scroll-down indicator ──
|
||||||
|
document.querySelectorAll('.mokojoomhero__scroll-indicator').forEach(function (btn) {
|
||||||
|
var hero = btn.closest('.mokojoomhero');
|
||||||
|
|
||||||
|
if (!hero) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var nextEl = hero.nextElementSibling || hero.parentElement.nextElementSibling;
|
||||||
|
|
||||||
|
if (nextEl) {
|
||||||
|
nextEl.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide indicator once hero scrolls out of view
|
||||||
|
var scrollObserver = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
btn.classList.add('mokojoomhero__scroll-indicator--hidden');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('mokojoomhero__scroll-indicator--hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.1 });
|
||||||
|
|
||||||
|
scrollObserver.observe(hero);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Mute/unmute toggle ──
|
// ── Mute/unmute toggle ──
|
||||||
document.querySelectorAll('.mokojoomhero__mute-toggle').forEach(function (btn) {
|
document.querySelectorAll('.mokojoomhero__mute-toggle').forEach(function (btn) {
|
||||||
var hero = btn.closest('.mokojoomhero');
|
var hero = btn.closest('.mokojoomhero');
|
||||||
|
|
||||||
|
if (!hero) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var video = hero.querySelector('video.mokojoomhero__video');
|
var video = hero.querySelector('video.mokojoomhero__video');
|
||||||
var iframe = hero.querySelector('iframe.mokojoomhero__video');
|
var iframe = hero.querySelector('iframe.mokojoomhero__video');
|
||||||
var icon = btn.querySelector('.mokojoomhero__mute-icon');
|
var icon = btn.querySelector('.mokojoomhero__mute-icon');
|
||||||
@@ -99,7 +276,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
if (video) {
|
if (video) {
|
||||||
video.muted = !muted;
|
video.muted = !muted;
|
||||||
}
|
}
|
||||||
if (iframe) {
|
if (iframe && iframe.contentWindow) {
|
||||||
var src = iframe.src || '';
|
var src = iframe.src || '';
|
||||||
if (src.indexOf('youtube') !== -1) {
|
if (src.indexOf('youtube') !== -1) {
|
||||||
var func = muted ? 'unMute' : 'mute';
|
var func = muted ? 'unMute' : 'mute';
|
||||||
@@ -112,7 +289,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
btn.setAttribute('data-muted', muted ? 'false' : 'true');
|
btn.setAttribute('data-muted', muted ? 'false' : 'true');
|
||||||
btn.setAttribute('aria-label', muted ? 'Mute video' : 'Unmute video');
|
btn.setAttribute('aria-label', muted ? 'Mute video' : 'Unmute video');
|
||||||
icon.textContent = muted ? '\u{1F50A}' : '\u{1F507}';
|
|
||||||
|
if (icon) {
|
||||||
|
icon.textContent = muted ? '\u{1F50A}' : '\u{1F507}';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+255
-35
@@ -6,6 +6,7 @@
|
|||||||
*
|
*
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
* @license GPL-3.0-or-later
|
* @license GPL-3.0-or-later
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
@@ -22,48 +23,206 @@ $wa = $app->getDocument()->getWebAssetManager();
|
|||||||
$wa->getRegistry()->addExtensionRegistryFile('mod_mokojoomhero');
|
$wa->getRegistry()->addExtensionRegistryFile('mod_mokojoomhero');
|
||||||
$wa->usePreset('mod_mokojoomhero');
|
$wa->usePreset('mod_mokojoomhero');
|
||||||
|
|
||||||
|
// Schedule check — skip rendering if outside the configured date range
|
||||||
|
$scheduleEnabled = (bool) $params->get('scheduleEnabled', 0);
|
||||||
|
|
||||||
|
if ($scheduleEnabled) {
|
||||||
|
$now = new \DateTime('now', new \DateTimeZone($app->get('offset', 'UTC')));
|
||||||
|
$scheduleStart = $params->get('scheduleStart', '');
|
||||||
|
$scheduleEnd = $params->get('scheduleEnd', '');
|
||||||
|
|
||||||
|
if ($scheduleStart) {
|
||||||
|
$start = new \DateTime($scheduleStart, new \DateTimeZone($app->get('offset', 'UTC')));
|
||||||
|
|
||||||
|
if ($now < $start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scheduleEnd) {
|
||||||
|
$end = new \DateTime($scheduleEnd, new \DateTimeZone($app->get('offset', 'UTC')));
|
||||||
|
|
||||||
|
if ($now > $end) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A/B testing — weighted random variation, session-sticky per module instance
|
||||||
|
$abEnabled = (bool) $params->get('abEnabled', 0);
|
||||||
|
$abVariationContent = '';
|
||||||
|
|
||||||
|
if ($abEnabled) {
|
||||||
|
$abVariations = $params->get('abVariations', '');
|
||||||
|
$abData = is_string($abVariations) ? json_decode($abVariations, true) : (array) $abVariations;
|
||||||
|
|
||||||
|
if (is_array($abData) && count($abData) > 0) {
|
||||||
|
$session = \Joomla\CMS\Factory::getSession();
|
||||||
|
$sessionKey = 'mokojoomhero.ab.' . $module->id;
|
||||||
|
$picked = $session->get($sessionKey, null);
|
||||||
|
|
||||||
|
if ($picked === null || !isset($abData[$picked])) {
|
||||||
|
// Weighted random selection
|
||||||
|
$totalWeight = 0;
|
||||||
|
|
||||||
|
foreach ($abData as $v) {
|
||||||
|
$totalWeight += (int) (((array) $v)['weight'] ?? 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rand = mt_rand(1, max($totalWeight, 1));
|
||||||
|
$cumulative = 0;
|
||||||
|
$picked = 0;
|
||||||
|
|
||||||
|
foreach ($abData as $i => $v) {
|
||||||
|
$cumulative += (int) (((array) $v)['weight'] ?? 50);
|
||||||
|
|
||||||
|
if ($rand <= $cumulative) {
|
||||||
|
$picked = $i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->set($sessionKey, $picked);
|
||||||
|
}
|
||||||
|
|
||||||
|
$variation = (array) ($abData[$picked] ?? []);
|
||||||
|
$abVariationContent = $variation['content'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Module parameters
|
// Module parameters
|
||||||
$heroMode = $params->get('heroMode', 'images');
|
$heroMode = $params->get('heroMode', 'images');
|
||||||
$imageFolder = $params->get('imageFolder', 'images/heroes');
|
$imageFolder = $params->get('imageFolder', 'images/heroes');
|
||||||
$imageCount = (int) $params->get('imageCount', 5);
|
$imageCount = (int) $params->get('imageCount', 5);
|
||||||
$slideInterval = (int) $params->get('slideInterval', 5000);
|
$slideInterval = (int) $params->get('slideInterval', 5000);
|
||||||
|
$fadeType = $params->get('fadeType', 'crossfade');
|
||||||
$videoFile = $params->get('videoFile', '');
|
$videoFile = $params->get('videoFile', '');
|
||||||
$heroHeight = $params->get('heroHeight', '60vh');
|
$heroHeight = $params->get('heroHeight', '60vh');
|
||||||
|
$heroHeightMobile = $params->get('heroHeightMobile', '');
|
||||||
$overlayColor = $params->get('overlayColor', '#000000');
|
$overlayColor = $params->get('overlayColor', '#000000');
|
||||||
|
$overlayType = $params->get('overlayType', 'solid');
|
||||||
$overlayOpacity = (float) $params->get('overlayOpacity', 0.5);
|
$overlayOpacity = (float) $params->get('overlayOpacity', 0.5);
|
||||||
$textAlign = $params->get('textAlign', 'center');
|
$textAlign = $params->get('textAlign', 'center');
|
||||||
|
$verticalAlign = $params->get('verticalAlign', 'center');
|
||||||
$textColor = $params->get('textColor', '#ffffff');
|
$textColor = $params->get('textColor', '#ffffff');
|
||||||
$heroContent = $params->get('heroContent', '');
|
$contentSource = $params->get('contentSource', 'manual');
|
||||||
|
$articleId = (int) $params->get('articleId', 0);
|
||||||
|
$useArticleTitle = (bool) $params->get('useArticleTitle', 0);
|
||||||
|
$heroContent = $params->get('heroContent', '');
|
||||||
|
$slideContent = $params->get('slideContent', '');
|
||||||
$showCard = (bool) $params->get('showCard', 1);
|
$showCard = (bool) $params->get('showCard', 1);
|
||||||
$cardDelay = (int) $params->get('cardDelay', 0);
|
$cardDelay = (int) $params->get('cardDelay', 0);
|
||||||
$showMuteToggle = (bool) $params->get('showMuteToggle', 0);
|
$contentAnimation = $params->get('contentAnimation', 'none');
|
||||||
$localVideoFile = $params->get('localVideoFile', '');
|
$contentAnimationDelay = (int) $params->get('contentAnimationDelay', 0);
|
||||||
|
$parallaxEnabled = (bool) $params->get('parallaxEnabled', 0);
|
||||||
|
$parallaxSpeed = (float) $params->get('parallaxSpeed', 0.5);
|
||||||
|
$showMuteToggle = (bool) $params->get('showMuteToggle', 0);
|
||||||
|
$videoPoster = $params->get('videoPoster', '');
|
||||||
|
$showScrollIndicator = (bool) $params->get('showScrollIndicator', 0);
|
||||||
|
$localVideoFile = $params->get('localVideoFile', '');
|
||||||
|
$bgColor = $params->get('bgColor', '#003366');
|
||||||
|
$gradientStart = $params->get('gradientStart', '#003366');
|
||||||
|
$gradientEnd = $params->get('gradientEnd', '#006699');
|
||||||
|
$gradientAngle = (int) $params->get('gradientAngle', 135);
|
||||||
|
|
||||||
|
// Validate CSS height values to prevent injection
|
||||||
|
if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) {
|
||||||
|
$heroHeight = '60vh';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($heroHeightMobile && !preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeightMobile)) {
|
||||||
|
$heroHeightMobile = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hex colour values
|
||||||
|
$hexColorPattern = '/^#[0-9a-fA-F]{6}$/';
|
||||||
|
|
||||||
|
if (!preg_match($hexColorPattern, $overlayColor)) {
|
||||||
|
$overlayColor = '#000000';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match($hexColorPattern, $textColor)) {
|
||||||
|
$textColor = '#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match($hexColorPattern, $bgColor)) {
|
||||||
|
$bgColor = '#003366';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match($hexColorPattern, $gradientStart)) {
|
||||||
|
$gradientStart = '#003366';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match($hexColorPattern, $gradientEnd)) {
|
||||||
|
$gradientEnd = '#006699';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate allowlist values
|
||||||
|
$allowedTextAlign = ['left', 'center', 'right'];
|
||||||
|
|
||||||
|
if (!in_array($textAlign, $allowedTextAlign, true)) {
|
||||||
|
$textAlign = 'center';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedFadeTypes = ['crossfade', 'slide', 'fade-black', 'zoom'];
|
||||||
|
|
||||||
|
if (!in_array($fadeType, $allowedFadeTypes, true)) {
|
||||||
|
$fadeType = 'crossfade';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedOverlayTypes = ['solid', 'gradient-bottom', 'gradient-top', 'gradient-left', 'gradient-right'];
|
||||||
|
|
||||||
|
if (!in_array($overlayType, $allowedOverlayTypes, true)) {
|
||||||
|
$overlayType = 'solid';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedContentAnimations = ['none', 'fade-in', 'slide-up', 'slide-left', 'slide-right'];
|
||||||
|
|
||||||
|
if (!in_array($contentAnimation, $allowedContentAnimations, true)) {
|
||||||
|
$contentAnimation = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
$parallaxSpeed = max(0.1, min(0.9, $parallaxSpeed));
|
||||||
|
$gradientAngle = max(0, min(360, $gradientAngle));
|
||||||
|
|
||||||
|
// Apply A/B variation content if active
|
||||||
|
if ($abEnabled && $abVariationContent) {
|
||||||
|
$heroContent = $abVariationContent;
|
||||||
|
}
|
||||||
|
|
||||||
// Collect hero images
|
// Collect hero images
|
||||||
$heroImages = [];
|
$heroImages = [];
|
||||||
|
|
||||||
if ($heroMode === 'images') {
|
if ($heroMode === 'images') {
|
||||||
$folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/');
|
$folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/');
|
||||||
|
|
||||||
if (is_dir($folderPath)) {
|
if (is_dir($folderPath)) {
|
||||||
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'];
|
try {
|
||||||
$all = [];
|
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'];
|
||||||
|
$all = [];
|
||||||
|
|
||||||
foreach (new DirectoryIterator($folderPath) as $file) {
|
foreach (new DirectoryIterator($folderPath) as $file) {
|
||||||
if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) {
|
if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) {
|
||||||
$all[] = $file->getFilename();
|
$all[] = $file->getFilename();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($all) {
|
if ($all) {
|
||||||
shuffle($all);
|
shuffle($all);
|
||||||
$picked = array_slice($all, 0, min($imageCount, 5));
|
$picked = array_slice($all, 0, min($imageCount, 5));
|
||||||
|
|
||||||
foreach ($picked as $filename) {
|
foreach ($picked as $filename) {
|
||||||
$heroImages[] = Uri::root() . $imageFolder . '/' . $filename;
|
$heroImages[] = Uri::root() . $imageFolder . '/' . $filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (\UnexpectedValueException $e) {
|
||||||
|
\Joomla\CMS\Log\Log::add(
|
||||||
|
'MokoJoomHero: Cannot read image folder "' . $folderPath . '": ' . $e->getMessage(),
|
||||||
|
\Joomla\CMS\Log\Log::WARNING,
|
||||||
|
'mod_mokojoomhero'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build video URL — smartly detect YouTube, Vimeo, or local/direct file
|
// Build video URL — smartly detect YouTube, Vimeo, or local/direct file
|
||||||
@@ -72,23 +231,84 @@ $youtubeId = '';
|
|||||||
$vimeoId = '';
|
$vimeoId = '';
|
||||||
|
|
||||||
if ($heroMode === 'localvideo' && $localVideoFile) {
|
if ($heroMode === 'localvideo' && $localVideoFile) {
|
||||||
$videoUrl = Uri::root() . ltrim($localVideoFile, '/');
|
$videoUrl = Uri::root() . ltrim($localVideoFile, '/');
|
||||||
} elseif ($heroMode === 'video' && $videoFile) {
|
} elseif ($heroMode === 'video' && $videoFile) {
|
||||||
// YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params
|
// YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params
|
||||||
if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) {
|
if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) {
|
||||||
$youtubeId = $m[1];
|
$youtubeId = $m[1];
|
||||||
// Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456
|
// Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456
|
||||||
} elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) {
|
} elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) {
|
||||||
$vimeoId = $m[1];
|
$vimeoId = $m[1];
|
||||||
} else {
|
} else {
|
||||||
// Direct URL or local file path
|
// Direct URL or local file path
|
||||||
$videoUrl = (strpos($videoFile, '://') !== false)
|
$videoUrl = (strpos($videoFile, '://') !== false)
|
||||||
? $videoFile
|
? $videoFile
|
||||||
: Uri::root() . ltrim($videoFile, '/');
|
: Uri::root() . ltrim($videoFile, '/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module content from the editor (overlay text)
|
// Load content from article if configured
|
||||||
$content = $module->content ?? '';
|
$articleTitle = '';
|
||||||
|
|
||||||
|
if ($contentSource === 'article' && $articleId > 0) {
|
||||||
|
try {
|
||||||
|
$db = \Joomla\CMS\Factory::getDbo();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName(['title', 'introtext', 'fulltext']))
|
||||||
|
->from($db->quoteName('#__content'))
|
||||||
|
->where($db->quoteName('id') . ' = ' . $articleId)
|
||||||
|
->where($db->quoteName('state') . ' = 1');
|
||||||
|
$db->setQuery($query);
|
||||||
|
$article = $db->loadObject();
|
||||||
|
|
||||||
|
if ($article) {
|
||||||
|
$rawContent = $article->introtext ?: $article->fulltext;
|
||||||
|
$heroContent = \Joomla\CMS\HTML\HTMLHelper::_('content.prepare', $rawContent);
|
||||||
|
$articleTitle = $article->title;
|
||||||
|
}
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
\Joomla\CMS\Log\Log::add(
|
||||||
|
'MokoJoomHero: Failed to load article ID ' . $articleId . ': ' . $e->getMessage(),
|
||||||
|
\Joomla\CMS\Log\Log::WARNING,
|
||||||
|
'mod_mokojoomhero'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process per-slide content — overrides folder-based images when populated
|
||||||
|
$slides = [];
|
||||||
|
|
||||||
|
if ($heroMode === 'images' && !empty($slideContent)) {
|
||||||
|
$slideData = is_string($slideContent) ? json_decode($slideContent, true) : (array) $slideContent;
|
||||||
|
|
||||||
|
if ($slideData === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
\Joomla\CMS\Log\Log::add(
|
||||||
|
'MokoJoomHero: Failed to decode slideContent JSON: ' . json_last_error_msg(),
|
||||||
|
\Joomla\CMS\Log\Log::WARNING,
|
||||||
|
'mod_mokojoomhero'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($slideData)) {
|
||||||
|
foreach ($slideData as $item) {
|
||||||
|
$item = (array) $item;
|
||||||
|
|
||||||
|
if (!empty($item['image'])) {
|
||||||
|
$slides[] = [
|
||||||
|
'image' => Uri::root() . ltrim($item['image'], '/'),
|
||||||
|
'heading' => $item['heading'] ?? '',
|
||||||
|
'body' => $item['body'] ?? '',
|
||||||
|
'link' => $item['link'] ?? '',
|
||||||
|
'linkText' => $item['linkText'] ?? 'Learn More',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-slide content overrides folder-based random images
|
||||||
|
if ($slides) {
|
||||||
|
$heroImages = array_column($slides, 'image');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default'));
|
require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default'));
|
||||||
|
|||||||
+317
-8
@@ -16,14 +16,14 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="module" client="site" method="upgrade">
|
<extension type="module" client="site" method="upgrade">
|
||||||
<name>Module - MokoJoomHero</name>
|
<name>Module - MokoJoomHero</name>
|
||||||
<creationDate>2026-05</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<version>01.07.00</version>
|
<version>01.19.00</version>
|
||||||
<description>Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting.</description>
|
<description>Random hero image slideshow, video backgrounds, solid colour/gradient, parallax, content animations, A/B testing, scheduling, and overlay with card support. Free and open source. By Moko Consulting.</description>
|
||||||
|
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
|
|
||||||
@@ -48,6 +48,10 @@
|
|||||||
<language tag="en-US">en-US/mod_mokojoomhero.sys.ini</language>
|
<language tag="en-US">en-US/mod_mokojoomhero.sys.ini</language>
|
||||||
</languages>
|
</languages>
|
||||||
|
|
||||||
|
<updateservers>
|
||||||
|
<server type="extension" name="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/updates.xml</server>
|
||||||
|
</updateservers>
|
||||||
|
|
||||||
<config>
|
<config>
|
||||||
<fields name="params">
|
<fields name="params">
|
||||||
<fieldset name="basic">
|
<fieldset name="basic">
|
||||||
@@ -61,6 +65,56 @@
|
|||||||
<option value="images">MOD_MOKOJOOMHERO_MODE_IMAGES</option>
|
<option value="images">MOD_MOKOJOOMHERO_MODE_IMAGES</option>
|
||||||
<option value="video">MOD_MOKOJOOMHERO_MODE_VIDEO</option>
|
<option value="video">MOD_MOKOJOOMHERO_MODE_VIDEO</option>
|
||||||
<option value="localvideo">MOD_MOKOJOOMHERO_MODE_LOCALVIDEO</option>
|
<option value="localvideo">MOD_MOKOJOOMHERO_MODE_LOCALVIDEO</option>
|
||||||
|
<option value="color">MOD_MOKOJOOMHERO_MODE_COLOR</option>
|
||||||
|
<option value="gradient">MOD_MOKOJOOMHERO_MODE_GRADIENT</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="bgColor"
|
||||||
|
type="color"
|
||||||
|
label="MOD_MOKOJOOMHERO_BG_COLOR_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_BG_COLOR_DESC"
|
||||||
|
default="#003366"
|
||||||
|
showon="heroMode:color"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="gradientStart"
|
||||||
|
type="color"
|
||||||
|
label="MOD_MOKOJOOMHERO_GRADIENT_START_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_GRADIENT_START_DESC"
|
||||||
|
default="#003366"
|
||||||
|
showon="heroMode:gradient"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="gradientEnd"
|
||||||
|
type="color"
|
||||||
|
label="MOD_MOKOJOOMHERO_GRADIENT_END_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_GRADIENT_END_DESC"
|
||||||
|
default="#006699"
|
||||||
|
showon="heroMode:gradient"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="gradientAngle"
|
||||||
|
type="number"
|
||||||
|
label="MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC"
|
||||||
|
default="135"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
step="15"
|
||||||
|
showon="heroMode:gradient"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="fadeType"
|
||||||
|
type="list"
|
||||||
|
label="MOD_MOKOJOOMHERO_FADE_TYPE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_FADE_TYPE_DESC"
|
||||||
|
default="crossfade"
|
||||||
|
showon="heroMode:images"
|
||||||
|
>
|
||||||
|
<option value="crossfade">MOD_MOKOJOOMHERO_FADE_CROSSFADE</option>
|
||||||
|
<option value="slide">MOD_MOKOJOOMHERO_FADE_SLIDE</option>
|
||||||
|
<option value="fade-black">MOD_MOKOJOOMHERO_FADE_BLACK</option>
|
||||||
|
<option value="zoom">MOD_MOKOJOOMHERO_FADE_ZOOM</option>
|
||||||
</field>
|
</field>
|
||||||
<field
|
<field
|
||||||
name="imageFolder"
|
name="imageFolder"
|
||||||
@@ -91,6 +145,51 @@
|
|||||||
step="500"
|
step="500"
|
||||||
showon="heroMode:images"
|
showon="heroMode:images"
|
||||||
/>
|
/>
|
||||||
|
<field
|
||||||
|
name="slideContent"
|
||||||
|
type="subform"
|
||||||
|
label="MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC"
|
||||||
|
multiple="true"
|
||||||
|
layout="joomla.form.field.subform.repeatable-table"
|
||||||
|
showon="heroMode:images"
|
||||||
|
max="5"
|
||||||
|
>
|
||||||
|
<form>
|
||||||
|
<field
|
||||||
|
name="image"
|
||||||
|
type="media"
|
||||||
|
label="MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL"
|
||||||
|
types="images"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="heading"
|
||||||
|
type="text"
|
||||||
|
label="MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL"
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="body"
|
||||||
|
type="textarea"
|
||||||
|
label="MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL"
|
||||||
|
filter="safehtml"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="link"
|
||||||
|
type="url"
|
||||||
|
label="MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL"
|
||||||
|
filter="url"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="linkText"
|
||||||
|
type="text"
|
||||||
|
label="MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL"
|
||||||
|
filter="string"
|
||||||
|
default="Learn More"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="videoFile"
|
name="videoFile"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -116,6 +215,56 @@
|
|||||||
default="60vh"
|
default="60vh"
|
||||||
filter="string"
|
filter="string"
|
||||||
/>
|
/>
|
||||||
|
<field
|
||||||
|
name="heroHeightMobile"
|
||||||
|
type="text"
|
||||||
|
label="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC"
|
||||||
|
hint="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="videoPoster"
|
||||||
|
type="media"
|
||||||
|
label="MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC"
|
||||||
|
types="images"
|
||||||
|
showon="heroMode:video,localvideo"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="showScrollIndicator"
|
||||||
|
type="radio"
|
||||||
|
layout="joomla.form.field.radio.switcher"
|
||||||
|
label="MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC"
|
||||||
|
default="0"
|
||||||
|
>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="parallaxEnabled"
|
||||||
|
type="radio"
|
||||||
|
layout="joomla.form.field.radio.switcher"
|
||||||
|
label="MOD_MOKOJOOMHERO_PARALLAX_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_PARALLAX_DESC"
|
||||||
|
default="0"
|
||||||
|
>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="parallaxSpeed"
|
||||||
|
type="range"
|
||||||
|
label="MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC"
|
||||||
|
default="0.5"
|
||||||
|
min="0.1"
|
||||||
|
max="0.9"
|
||||||
|
step="0.1"
|
||||||
|
showon="parallaxEnabled:1"
|
||||||
|
/>
|
||||||
<field
|
<field
|
||||||
name="showMuteToggle"
|
name="showMuteToggle"
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -129,9 +278,102 @@
|
|||||||
<option value="1">JYES</option>
|
<option value="1">JYES</option>
|
||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset name="abtesting"
|
||||||
|
label="MOD_MOKOJOOMHERO_FIELDSET_AB"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="abEnabled"
|
||||||
|
type="radio"
|
||||||
|
layout="joomla.form.field.radio.switcher"
|
||||||
|
label="MOD_MOKOJOOMHERO_AB_ENABLED_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_AB_ENABLED_DESC"
|
||||||
|
default="0"
|
||||||
|
>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="abVariations"
|
||||||
|
type="subform"
|
||||||
|
label="MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC"
|
||||||
|
multiple="true"
|
||||||
|
layout="joomla.form.field.subform.repeatable-table"
|
||||||
|
showon="abEnabled:1"
|
||||||
|
max="4"
|
||||||
|
>
|
||||||
|
<form>
|
||||||
|
<field
|
||||||
|
name="label"
|
||||||
|
type="text"
|
||||||
|
label="MOD_MOKOJOOMHERO_AB_VAR_LABEL"
|
||||||
|
filter="string"
|
||||||
|
default="Variation"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="content"
|
||||||
|
type="textarea"
|
||||||
|
label="MOD_MOKOJOOMHERO_AB_VAR_CONTENT"
|
||||||
|
filter="safehtml"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="weight"
|
||||||
|
type="number"
|
||||||
|
label="MOD_MOKOJOOMHERO_AB_VAR_WEIGHT"
|
||||||
|
default="50"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset name="scheduling"
|
||||||
|
label="MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING"
|
||||||
|
>
|
||||||
|
<field
|
||||||
|
name="scheduleEnabled"
|
||||||
|
type="radio"
|
||||||
|
layout="joomla.form.field.radio.switcher"
|
||||||
|
label="MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC"
|
||||||
|
default="0"
|
||||||
|
>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="scheduleStart"
|
||||||
|
type="calendar"
|
||||||
|
label="MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_SCHEDULE_START_DESC"
|
||||||
|
format="%Y-%m-%d %H:%M"
|
||||||
|
showtime="true"
|
||||||
|
showon="scheduleEnabled:1"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="scheduleEnd"
|
||||||
|
type="calendar"
|
||||||
|
label="MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_SCHEDULE_END_DESC"
|
||||||
|
format="%Y-%m-%d %H:%M"
|
||||||
|
showtime="true"
|
||||||
|
showon="scheduleEnabled:1"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
<fieldset name="content"
|
<fieldset name="content"
|
||||||
label="MOD_MOKOJOOMHERO_FIELDSET_CONTENT"
|
label="MOD_MOKOJOOMHERO_FIELDSET_CONTENT"
|
||||||
>
|
>
|
||||||
|
<field
|
||||||
|
name="contentSource"
|
||||||
|
type="list"
|
||||||
|
label="MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC"
|
||||||
|
default="manual"
|
||||||
|
>
|
||||||
|
<option value="manual">MOD_MOKOJOOMHERO_SOURCE_MANUAL</option>
|
||||||
|
<option value="article">MOD_MOKOJOOMHERO_SOURCE_ARTICLE</option>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="heroContent"
|
name="heroContent"
|
||||||
type="editor"
|
type="editor"
|
||||||
@@ -140,7 +382,31 @@
|
|||||||
filter="safehtml"
|
filter="safehtml"
|
||||||
buttons="true"
|
buttons="true"
|
||||||
hide="readmore,pagebreak"
|
hide="readmore,pagebreak"
|
||||||
|
showon="contentSource:manual"
|
||||||
/>
|
/>
|
||||||
|
<field
|
||||||
|
name="articleId"
|
||||||
|
type="sql"
|
||||||
|
label="MOD_MOKOJOOMHERO_ARTICLE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_ARTICLE_DESC"
|
||||||
|
query="SELECT id, title FROM #__content WHERE state = 1 ORDER BY title ASC"
|
||||||
|
key_field="id"
|
||||||
|
value_field="title"
|
||||||
|
header="MOD_MOKOJOOMHERO_ARTICLE_SELECT"
|
||||||
|
showon="contentSource:article"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="useArticleTitle"
|
||||||
|
type="radio"
|
||||||
|
layout="joomla.form.field.radio.switcher"
|
||||||
|
label="MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC"
|
||||||
|
default="0"
|
||||||
|
showon="contentSource:article"
|
||||||
|
>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="showCard"
|
name="showCard"
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -152,6 +418,30 @@
|
|||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
<option value="1">JYES</option>
|
<option value="1">JYES</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="contentAnimation"
|
||||||
|
type="list"
|
||||||
|
label="MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC"
|
||||||
|
default="none"
|
||||||
|
>
|
||||||
|
<option value="none">MOD_MOKOJOOMHERO_ANIM_NONE</option>
|
||||||
|
<option value="fade-in">MOD_MOKOJOOMHERO_ANIM_FADE_IN</option>
|
||||||
|
<option value="slide-up">MOD_MOKOJOOMHERO_ANIM_SLIDE_UP</option>
|
||||||
|
<option value="slide-left">MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT</option>
|
||||||
|
<option value="slide-right">MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="contentAnimationDelay"
|
||||||
|
type="number"
|
||||||
|
label="MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC"
|
||||||
|
default="0"
|
||||||
|
min="0"
|
||||||
|
max="3000"
|
||||||
|
step="100"
|
||||||
|
showon="contentAnimation!:none"
|
||||||
|
/>
|
||||||
<field
|
<field
|
||||||
name="cardDelay"
|
name="cardDelay"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -174,6 +464,19 @@
|
|||||||
description="MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC"
|
description="MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC"
|
||||||
default="#000000"
|
default="#000000"
|
||||||
/>
|
/>
|
||||||
|
<field
|
||||||
|
name="overlayType"
|
||||||
|
type="list"
|
||||||
|
label="MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC"
|
||||||
|
default="solid"
|
||||||
|
>
|
||||||
|
<option value="solid">MOD_MOKOJOOMHERO_OVERLAY_SOLID</option>
|
||||||
|
<option value="gradient-bottom">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM</option>
|
||||||
|
<option value="gradient-top">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP</option>
|
||||||
|
<option value="gradient-left">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT</option>
|
||||||
|
<option value="gradient-right">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT</option>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="overlayOpacity"
|
name="overlayOpacity"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -195,6 +498,17 @@
|
|||||||
<option value="center">MOD_MOKOJOOMHERO_ALIGN_CENTER</option>
|
<option value="center">MOD_MOKOJOOMHERO_ALIGN_CENTER</option>
|
||||||
<option value="right">MOD_MOKOJOOMHERO_ALIGN_RIGHT</option>
|
<option value="right">MOD_MOKOJOOMHERO_ALIGN_RIGHT</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="verticalAlign"
|
||||||
|
type="list"
|
||||||
|
label="MOD_MOKOJOOMHERO_VALIGN_LABEL"
|
||||||
|
description="MOD_MOKOJOOMHERO_VALIGN_DESC"
|
||||||
|
default="center"
|
||||||
|
>
|
||||||
|
<option value="top">MOD_MOKOJOOMHERO_VALIGN_TOP</option>
|
||||||
|
<option value="center">MOD_MOKOJOOMHERO_VALIGN_CENTER</option>
|
||||||
|
<option value="bottom">MOD_MOKOJOOMHERO_VALIGN_BOTTOM</option>
|
||||||
|
</field>
|
||||||
<field
|
<field
|
||||||
name="textColor"
|
name="textColor"
|
||||||
type="color"
|
type="color"
|
||||||
@@ -206,10 +520,5 @@
|
|||||||
</fields>
|
</fields>
|
||||||
</config>
|
</config>
|
||||||
|
|
||||||
<updateservers>
|
|
||||||
<server type="extension" priority="1" name="MokoJoomHero Updates">
|
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml
|
|
||||||
</server>
|
|
||||||
</updateservers>
|
|
||||||
</extension>
|
</extension>
|
||||||
|
|
||||||
|
|||||||
+39
-7
@@ -6,21 +6,53 @@
|
|||||||
*
|
*
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
* @license GPL-3.0-or-later
|
* @license GPL-3.0-or-later
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Filesystem\Folder;
|
use Joomla\CMS\Filesystem\Folder;
|
||||||
use Joomla\CMS\Installer\InstallerAdapter;
|
use Joomla\CMS\Installer\InstallerAdapter;
|
||||||
|
|
||||||
class Mod_MokojoomheroInstallerScript
|
class Mod_MokojoomheroInstallerScript
|
||||||
{
|
{
|
||||||
public function postflight(string $type, InstallerAdapter $adapter): void
|
public function postflight(string $type, InstallerAdapter $adapter): void
|
||||||
{
|
{
|
||||||
$heroesPath = JPATH_ROOT . '/images/heroes';
|
// Create default image folder on fresh install
|
||||||
|
$heroesPath = JPATH_ROOT . '/images/heroes';
|
||||||
|
|
||||||
if (!is_dir($heroesPath)) {
|
if (!is_dir($heroesPath)) {
|
||||||
Folder::create($heroesPath);
|
Folder::create($heroesPath);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Remove deprecated system plugin (was part of the old package extension)
|
||||||
|
$this->removeDeprecatedPlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall plg_system_mokojoomhero if it exists — no longer needed.
|
||||||
|
*/
|
||||||
|
private function removeDeprecatedPlugin(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('extension_id'))
|
||||||
|
->from($db->quoteName('#__extensions'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||||
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero'));
|
||||||
|
$db->setQuery($query);
|
||||||
|
$extensionId = (int) $db->loadResult();
|
||||||
|
|
||||||
|
if ($extensionId) {
|
||||||
|
$installer = new \Joomla\CMS\Installer\Installer();
|
||||||
|
$installer->uninstall('plugin', $extensionId);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Non-critical — plugin may already be gone
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-15
@@ -6,52 +6,114 @@
|
|||||||
*
|
*
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
* @license GPL-3.0-or-later
|
* @license GPL-3.0-or-later
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
|
|
||||||
/** @var string $heroMode */
|
/** @var string $heroMode */
|
||||||
/** @var array $heroImages */
|
/** @var array $heroImages */
|
||||||
/** @var int $slideInterval */
|
/** @var int $slideInterval */
|
||||||
|
/** @var string $fadeType */
|
||||||
/** @var string $videoUrl */
|
/** @var string $videoUrl */
|
||||||
/** @var string $youtubeId */
|
/** @var string $youtubeId */
|
||||||
/** @var string $vimeoId */
|
/** @var string $vimeoId */
|
||||||
/** @var string $heroHeight */
|
/** @var string $heroHeight */
|
||||||
|
/** @var string $heroHeightMobile */
|
||||||
/** @var string $overlayColor */
|
/** @var string $overlayColor */
|
||||||
|
/** @var string $overlayType */
|
||||||
/** @var float $overlayOpacity */
|
/** @var float $overlayOpacity */
|
||||||
/** @var string $textAlign */
|
/** @var string $textAlign */
|
||||||
|
/** @var string $verticalAlign */
|
||||||
/** @var string $textColor */
|
/** @var string $textColor */
|
||||||
|
/** @var string $contentSource */
|
||||||
|
/** @var int $articleId */
|
||||||
|
/** @var bool $useArticleTitle */
|
||||||
/** @var string $heroContent */
|
/** @var string $heroContent */
|
||||||
|
/** @var string $articleTitle */
|
||||||
|
/** @var array $slides */
|
||||||
/** @var bool $showCard */
|
/** @var bool $showCard */
|
||||||
/** @var int $cardDelay */
|
/** @var int $cardDelay */
|
||||||
|
/** @var string $contentAnimation */
|
||||||
|
/** @var int $contentAnimationDelay */
|
||||||
|
/** @var bool $parallaxEnabled */
|
||||||
|
/** @var float $parallaxSpeed */
|
||||||
/** @var bool $showMuteToggle */
|
/** @var bool $showMuteToggle */
|
||||||
/** @var string $content */
|
/** @var string $videoPoster */
|
||||||
|
/** @var bool $showScrollIndicator */
|
||||||
|
/** @var string $bgColor */
|
||||||
|
/** @var string $gradientStart */
|
||||||
|
/** @var string $gradientEnd */
|
||||||
|
/** @var int $gradientAngle */
|
||||||
$moduleId = 'mod-mokojoomhero-' . $module->id;
|
$moduleId = 'mod-mokojoomhero-' . $module->id;
|
||||||
|
|
||||||
// Convert hex overlay colour to rgba
|
// Convert hex overlay colour to rgba
|
||||||
$r = hexdec(substr($overlayColor, 1, 2));
|
$r = hexdec(substr($overlayColor, 1, 2));
|
||||||
$g = hexdec(substr($overlayColor, 3, 2));
|
$g = hexdec(substr($overlayColor, 3, 2));
|
||||||
$b = hexdec(substr($overlayColor, 5, 2));
|
$b = hexdec(substr($overlayColor, 5, 2));
|
||||||
$rgba = "rgba($r, $g, $b, $overlayOpacity)";
|
$rgbaOpaque = "rgba($r, $g, $b, $overlayOpacity)";
|
||||||
|
$rgbaTransparent = "rgba($r, $g, $b, 0)";
|
||||||
|
|
||||||
|
// Build overlay background based on type
|
||||||
|
$overlayDirections = [
|
||||||
|
'gradient-bottom' => 'to bottom',
|
||||||
|
'gradient-top' => 'to top',
|
||||||
|
'gradient-left' => 'to left',
|
||||||
|
'gradient-right' => 'to right',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Skip overlay on solid colour/gradient modes — background is already a controlled design choice
|
||||||
|
if ($heroMode === 'color' || $heroMode === 'gradient') {
|
||||||
|
$overlayBg = '';
|
||||||
|
} elseif ($overlayType !== 'solid' && isset($overlayDirections[$overlayType])) {
|
||||||
|
$dir = $overlayDirections[$overlayType];
|
||||||
|
$overlayBg = "background: linear-gradient($dir, $rgbaTransparent, $rgbaOpaque);";
|
||||||
|
} else {
|
||||||
|
$overlayBg = "background-color: $rgbaOpaque;";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map vertical alignment to CSS align-items
|
||||||
|
$valignMap = ['top' => 'flex-start', 'center' => 'center', 'bottom' => 'flex-end'];
|
||||||
|
$valignCss = $valignMap[$verticalAlign] ?? 'center';
|
||||||
|
|
||||||
$heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
|
$heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
|
||||||
?>
|
?>
|
||||||
|
<?php if ($heroHeightMobile) : ?>
|
||||||
|
<style>#<?php echo $moduleId; ?> { --mokojoomhero-mobile-height: <?php echo htmlspecialchars($heroHeightMobile, ENT_QUOTES, 'UTF-8'); ?>; }</style>
|
||||||
|
<?php endif; ?>
|
||||||
<div id="<?php echo $moduleId; ?>" class="mokojoomhero" style="height: <?php echo $heightAttr; ?>;"
|
<div id="<?php echo $moduleId; ?>" class="mokojoomhero" style="height: <?php echo $heightAttr; ?>;"
|
||||||
|
<?php if ($parallaxEnabled) : ?>
|
||||||
|
data-parallax="<?php echo $parallaxSpeed; ?>"
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($heroMode === 'images' && count($heroImages) > 1) : ?>
|
<?php if ($heroMode === 'images' && count($heroImages) > 1) : ?>
|
||||||
data-slides="<?php echo htmlspecialchars(json_encode($heroImages), ENT_QUOTES, 'UTF-8'); ?>"
|
data-slides="<?php echo htmlspecialchars(json_encode($heroImages), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
data-interval="<?php echo $slideInterval; ?>"
|
data-interval="<?php echo $slideInterval; ?>"
|
||||||
|
data-transition="<?php echo htmlspecialchars($fadeType, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?php if ($slides) : ?>
|
||||||
|
data-slide-content="<?php echo htmlspecialchars(json_encode($slides), ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
>
|
>
|
||||||
<?php // Background layer — single image, slideshow, or video ?>
|
<?php // Background layer — solid colour, single image, slideshow, or video ?>
|
||||||
<?php if ($heroMode === 'video' && $youtubeId) : ?>
|
<?php if ($heroMode === 'color') : ?>
|
||||||
|
<div class="mokojoomhero__color" style="background-color: <?php echo htmlspecialchars($bgColor, ENT_QUOTES, 'UTF-8'); ?>;"></div>
|
||||||
|
<?php elseif ($heroMode === 'gradient') : ?>
|
||||||
|
<div class="mokojoomhero__color" style="background: linear-gradient(<?php echo $gradientAngle; ?>deg, <?php echo htmlspecialchars($gradientStart, ENT_QUOTES, 'UTF-8'); ?>, <?php echo htmlspecialchars($gradientEnd, ENT_QUOTES, 'UTF-8'); ?>);"></div>
|
||||||
|
<?php elseif ($heroMode === 'video' && $youtubeId) : ?>
|
||||||
|
<?php if ($videoPoster) : ?>
|
||||||
|
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
|
||||||
|
<?php endif; ?>
|
||||||
<iframe class="mokojoomhero__video" src="https://www.youtube-nocookie.com/embed/<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&mute=1&loop=1&playlist=<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1&origin=<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root(), ENT_QUOTES, 'UTF-8'); ?>" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
<iframe class="mokojoomhero__video" src="https://www.youtube-nocookie.com/embed/<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&mute=1&loop=1&playlist=<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1&origin=<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root(), ENT_QUOTES, 'UTF-8'); ?>" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||||
<?php elseif ($heroMode === 'video' && $vimeoId) : ?>
|
<?php elseif ($heroMode === 'video' && $vimeoId) : ?>
|
||||||
|
<?php if ($videoPoster) : ?>
|
||||||
|
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
|
||||||
|
<?php endif; ?>
|
||||||
<iframe class="mokojoomhero__video" src="https://player.vimeo.com/video/<?php echo htmlspecialchars($vimeoId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&muted=1&loop=1&background=1" allow="autoplay" allowfullscreen></iframe>
|
<iframe class="mokojoomhero__video" src="https://player.vimeo.com/video/<?php echo htmlspecialchars($vimeoId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&muted=1&loop=1&background=1" allow="autoplay" allowfullscreen></iframe>
|
||||||
<?php elseif (($heroMode === 'video' || $heroMode === 'localvideo') && $videoUrl) : ?>
|
<?php elseif (($heroMode === 'video' || $heroMode === 'localvideo') && $videoUrl) : ?>
|
||||||
<video class="mokojoomhero__video" autoplay muted loop playsinline>
|
<?php if ($videoPoster) : ?>
|
||||||
|
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<video class="mokojoomhero__video" autoplay muted loop playsinline<?php if ($videoPoster) : ?> poster="<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>"<?php endif; ?>>
|
||||||
<source src="<?php echo htmlspecialchars($videoUrl, ENT_QUOTES, 'UTF-8'); ?>">
|
<source src="<?php echo htmlspecialchars($videoUrl, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
</video>
|
</video>
|
||||||
<?php elseif ($heroImages) : ?>
|
<?php elseif ($heroImages) : ?>
|
||||||
@@ -69,20 +131,32 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
|
|||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($showScrollIndicator) : ?>
|
||||||
|
<button class="mokojoomhero__scroll-indicator" type="button" aria-label="Scroll down">
|
||||||
|
<svg class="mokojoomhero__scroll-chevron" viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php // Overlay + content ?>
|
<?php // Overlay + content ?>
|
||||||
<div class="mokojoomhero__overlay" style="background-color: <?php echo $rgba; ?>;">
|
<div class="mokojoomhero__overlay" style="<?php echo $overlayBg; ?> align-items: <?php echo $valignCss; ?>;">
|
||||||
<div class="mokojoomhero__content" style="text-align: <?php echo htmlspecialchars($textAlign, ENT_QUOTES, 'UTF-8'); ?>; color: <?php echo htmlspecialchars($textColor, ENT_QUOTES, 'UTF-8'); ?>;">
|
<div class="mokojoomhero__content<?php if ($contentAnimation !== 'none') : ?> mokojoomhero__content--anim-<?php echo htmlspecialchars($contentAnimation, ENT_QUOTES, 'UTF-8'); ?><?php endif; ?>" style="text-align: <?php echo htmlspecialchars($textAlign, ENT_QUOTES, 'UTF-8'); ?>; color: <?php echo htmlspecialchars($textColor, ENT_QUOTES, 'UTF-8'); ?>;<?php if ($contentAnimationDelay) : ?> animation-delay: <?php echo $contentAnimationDelay; ?>ms;<?php endif; ?>">
|
||||||
<?php if ($heroContent || $module->showtitle) : ?>
|
<?php
|
||||||
|
$displayTitle = ($contentSource === 'article' && $useArticleTitle && $articleTitle)
|
||||||
|
? $articleTitle
|
||||||
|
: $module->title;
|
||||||
|
$showTitle = ($contentSource === 'article' && $useArticleTitle && $articleTitle) || $module->showtitle;
|
||||||
|
?>
|
||||||
|
<?php if ($heroContent || $showTitle) : ?>
|
||||||
<?php if ($showCard) : ?>
|
<?php if ($showCard) : ?>
|
||||||
<div class="mokojoomhero__card"<?php if ($cardDelay) : ?> style="animation-delay: <?php echo $cardDelay; ?>ms;" data-card-delay="<?php echo $cardDelay; ?>"<?php endif; ?>>
|
<div class="mokojoomhero__card"<?php if ($cardDelay) : ?> style="animation-delay: <?php echo $cardDelay; ?>ms;" data-card-delay="<?php echo $cardDelay; ?>"<?php endif; ?>>
|
||||||
<?php if ($module->showtitle) : ?>
|
<?php if ($showTitle) : ?>
|
||||||
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($module->title, ENT_QUOTES, 'UTF-8'); ?></h2>
|
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php echo $heroContent; ?>
|
<?php echo $heroContent; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<?php if ($module->showtitle) : ?>
|
<?php if ($showTitle) : ?>
|
||||||
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($module->title, ENT_QUOTES, 'UTF-8'); ?></h2>
|
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?></h2>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php echo $heroContent; ?>
|
<?php echo $heroContent; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Joomla Extension Update Server XML
|
|
||||||
See: https://docs.joomla.org/Deploying_an_Update_Server
|
|
||||||
|
|
||||||
This file is the update server manifest for mod_mokojoomhero.
|
|
||||||
The Joomla installer polls this URL to check for new versions.
|
|
||||||
|
|
||||||
The manifest in this repository must reference this file:
|
|
||||||
<updateservers>
|
|
||||||
<server type="extension" priority="1" name="MokoJoomHero Updates">
|
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml
|
|
||||||
</server>
|
|
||||||
</updateservers>
|
|
||||||
|
|
||||||
When a new release is made, run `make release` or the release workflow to
|
|
||||||
prepend a new <update> entry to this file automatically.
|
|
||||||
-->
|
|
||||||
<updates>
|
|
||||||
<update>
|
|
||||||
<name>Module - MokoJoomHero</name>
|
|
||||||
<description>MokoJoomHero — A Joomla hero image module by Moko Consulting</description>
|
|
||||||
<element>mod_mokojoomhero</element>
|
|
||||||
<type>module</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>{{VERSION}}</version>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">
|
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/v{{VERSION}}/mod_mokojoomhero.zip
|
|
||||||
</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<targetplatform name="joomla" version="(5|6).*" />
|
|
||||||
<php_minimum>8.1</php_minimum>
|
|
||||||
</update>
|
|
||||||
</updates>
|
|
||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
VERSION: 01.07.00
|
|
||||||
-->
|
|
||||||
|
|
||||||
<updates>
|
|
||||||
<update>
|
|
||||||
<name>MokoJoomHero</name>
|
|
||||||
<description>MokoJoomHero stable build.</description>
|
|
||||||
<element>mod_mokojoomhero</element>
|
|
||||||
<type>module</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>01.06.00</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/stable</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/stable/mod_mokojoomhero-01.06.00.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>de21e4010e19323746c9aeff12d6a240dc29a2a0c3ef1e091549a2331e710bc7</sha256>
|
|
||||||
<tags><tag>stable</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Module - MokoJoomHero</name>
|
|
||||||
<description>Module - MokoJoomHero dev build.</description>
|
|
||||||
<element>mod_mokojoomhero</element>
|
|
||||||
<type>module</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>01.07.00-dev</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/development</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/development/mod_mokojoomhero-01.07.00-dev.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
|
|
||||||
<tags><tag>dev</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Module - MokoJoomHero</name>
|
|
||||||
<description>Module - MokoJoomHero alpha build.</description>
|
|
||||||
<element>mod_mokojoomhero</element>
|
|
||||||
<type>module</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>01.07.00-alpha</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/alpha</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/alpha/mod_mokojoomhero-01.07.00-alpha.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
|
|
||||||
<tags><tag>alpha</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Module - MokoJoomHero</name>
|
|
||||||
<description>Module - MokoJoomHero beta build.</description>
|
|
||||||
<element>mod_mokojoomhero</element>
|
|
||||||
<type>module</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>01.07.00-beta</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/beta</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/beta/mod_mokojoomhero-01.07.00-beta.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
|
|
||||||
<tags><tag>beta</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Module - MokoJoomHero</name>
|
|
||||||
<description>Module - MokoJoomHero rc build.</description>
|
|
||||||
<element>mod_mokojoomhero</element>
|
|
||||||
<type>module</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>01.07.00-rc</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title='Module - MokoJoomHero'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/release-candidate</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/release-candidate/mod_mokojoomhero-01.07.00-rc.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
|
|
||||||
<tags><tag>rc</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
|
||||||
</update>
|
|
||||||
</updates>
|
|
||||||
Reference in New Issue
Block a user