diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..2e87263 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,57 @@ +name: Deploy Script Gallery + +on: + push: + branches: [main] + paths: + - 'ASSETS/**' + - 'scripts.json' + - 'tools/generate_site.py' + - '.github/workflows/gh-pages.yml' + pull_request: + paths: + - 'ASSETS/**' + - 'scripts.json' + - 'tools/generate_site.py' + - '.github/workflows/gh-pages.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.1.0 + - name: Generate site + run: uv run tools/generate_site.py --scripts-json scripts.json --assets-dir ASSETS --output-dir site + - name: Upload Pages artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: ./site + - name: Upload PR preview artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: site-preview-pr-${{ github.event.pull_request.number }} + path: ./site + + deploy: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index e43b0f9..2dc3b96 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,13 @@ .DS_Store +site/ + +# Python +__pycache__/ +*.py[cod] +*$py.class + +# Python virtual environments +.venv/ +venv/ +env/ +ENV/ diff --git a/ASSETS/edgetx-goodies/screenshot-5.png b/ASSETS/edgetx-goodies/screenshot-5.png new file mode 100644 index 0000000..a8b5754 Binary files /dev/null and b/ASSETS/edgetx-goodies/screenshot-5.png differ diff --git a/ASSETS/edgetx-goodies/screenshot-6.png b/ASSETS/edgetx-goodies/screenshot-6.png new file mode 100644 index 0000000..9e32ea7 Binary files /dev/null and b/ASSETS/edgetx-goodies/screenshot-6.png differ diff --git a/ASSETS/edgetx-goodies/screenshot-7.png b/ASSETS/edgetx-goodies/screenshot-7.png new file mode 100644 index 0000000..9c9fb92 Binary files /dev/null and b/ASSETS/edgetx-goodies/screenshot-7.png differ diff --git a/ASSETS/edgetx-goodies/screenshot-8.png b/ASSETS/edgetx-goodies/screenshot-8.png new file mode 100644 index 0000000..ebd462b Binary files /dev/null and b/ASSETS/edgetx-goodies/screenshot-8.png differ diff --git a/scripts.json b/scripts.json index 4fde5f3..641c91a 100644 --- a/scripts.json +++ b/scripts.json @@ -382,7 +382,9 @@ "ASSETS/edgetx-goodies/screenshot-1.png", "ASSETS/edgetx-goodies/screenshot-3.png", "ASSETS/edgetx-goodies/screenshot-5.png", - "https://github.com/MadMonkey87/EdgeTX-Goodies/raw/main/SCREENSHOTS/screenshot_tx16s_22-08-09_19-54-08.png" + "ASSETS/edgetx-goodies/screenshot-6.png", + "ASSETS/edgetx-goodies/screenshot-7.png", + "ASSETS/edgetx-goodies/screenshot-8.png" ], "tags": [ "widget", diff --git a/tools/generate_site.py b/tools/generate_site.py new file mode 100644 index 0000000..2b4233f --- /dev/null +++ b/tools/generate_site.py @@ -0,0 +1,627 @@ +#!/usr/bin/env python3 +""" +Generate a static gallery website for EdgeTX Lua scripts. + +Reads scripts.json, processes images (copy local, preserve URLs), +and generates a self-contained index.html with Tailwind + Alpine.js. +""" + +import json +import sys +import argparse +import shutil +from pathlib import Path + + +def load_scripts(scripts_json_path: Path) -> list: + """Load and validate scripts.json.""" + if not scripts_json_path.exists(): + print(f"Error: {scripts_json_path} not found", file=sys.stderr) + sys.exit(1) + + with open(scripts_json_path, 'r', encoding='utf-8') as f: + scripts = json.load(f) + + if not isinstance(scripts, list): + print("Error: scripts.json must be an array", file=sys.stderr) + sys.exit(1) + + return scripts + + +def process_images(scripts: list, assets_dir: Path, output_dir: Path) -> None: + """ + Process images for each script: + - Copy local files from assets_dir to output_dir/images/ + - Preserve external URLs (http/https) + - Update script['images'] with new paths + """ + images_output = output_dir / "images" + images_output.mkdir(parents=True, exist_ok=True) + + for script in scripts: + if 'images' not in script: + script['images'] = [] + continue + + processed_images = [] + + for image_path in script['images']: + # External URL: keep as-is + if image_path.startswith('http://') or image_path.startswith('https://'): + processed_images.append(image_path) + continue + + # Local file: copy and rewrite path + # image_path is repo-relative (e.g. "ASSETS/foo/shot.jpg"), + # so resolve from assets_dir's parent (the repo root), not assets_dir itself. + repo_root = assets_dir.parent.resolve() + source_path = (repo_root / image_path).resolve() + + # Reject paths outside the repository root + try: + source_path.relative_to(repo_root) + except ValueError: + print( + f"Warning: Skipping out-of-repo image path for '{script.get('name', 'unknown')}': {image_path}", + file=sys.stderr + ) + continue + + if not source_path.exists(): + print( + f"Warning: Image not found for '{script.get('name', 'unknown')}': {image_path}", + file=sys.stderr + ) + continue + + # Compute destination: preserve full relative subdirectory structure + # e.g., ASSETS/foo/bar/screenshot-1.jpg -> images/ASSETS/foo/bar/screenshot-1.jpg + relative_path = Path(image_path) + dest_path = images_output / relative_path + dest_path.parent.mkdir(parents=True, exist_ok=True) + + try: + shutil.copy2(source_path, dest_path) + # Store relative path from the perspective of output_dir root + new_image_path = str(dest_path.relative_to(output_dir)).replace('\\', '/') + processed_images.append(new_image_path) + except OSError as e: + print( + f"Warning: Failed to copy image '{image_path}' for '{script.get('name', 'unknown')}': {e}", + file=sys.stderr + ) + + script['images'] = processed_images + + +def get_categories(scripts: list) -> list: + """Extract and sort unique categories.""" + categories = set() + for script in scripts: + if 'category' in script: + categories.add(script['category']) + return sorted(categories) + + +def get_all_tags(scripts: list) -> list: + """Extract and sort unique tags across all scripts.""" + tags = set() + for script in scripts: + if 'tags' in script and isinstance(script['tags'], list): + tags.update(script['tags']) + return sorted(tags) + + +def generate_html(scripts: list, categories: list, all_tags: list) -> str: + """Generate the complete HTML file.""" + + # Convert data to JSON strings + def _safe_json(obj) -> str: + return json.dumps(obj, ensure_ascii=False).replace('<', '\\u003c') + + scripts_json = _safe_json(scripts) + categories_json = _safe_json(categories) + all_tags_json = _safe_json(all_tags) + + # HTML template with placeholders (avoiding f-string brace conflicts) + html_template = """ + + + + + EdgeTX Lua Scripts Gallery + + + + + + + + +
+
+
+

EdgeTX Lua Scripts

+ +
+ +
+ +
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + + +
+ + +
+ +
+ + + + +""" + + # Replace placeholders with actual JSON + html_output = html_template.replace('___SCRIPTS_JSON___', scripts_json) + html_output = html_output.replace('___CATEGORIES_JSON___', categories_json) + html_output = html_output.replace('___ALL_TAGS_JSON___', all_tags_json) + + return html_output + + +def main(): + parser = argparse.ArgumentParser( + description='Generate a static gallery website for EdgeTX Lua scripts.' + ) + parser.add_argument( + '--scripts-json', + type=Path, + default=Path('scripts.json'), + help='Path to scripts.json (default: scripts.json)' + ) + parser.add_argument( + '--assets-dir', + type=Path, + default=Path('ASSETS'), + help='Path to assets directory (default: ASSETS)' + ) + parser.add_argument( + '--output-dir', + type=Path, + default=Path('site'), + help='Output directory for generated site (default: site)' + ) + + args = parser.parse_args() + + # Resolve paths relative to CWD + scripts_json_path = args.scripts_json.resolve() + assets_dir = args.assets_dir.resolve() + output_dir = args.output_dir.resolve() + + # Create output directory + output_dir.mkdir(parents=True, exist_ok=True) + + # Load scripts + scripts = load_scripts(scripts_json_path) + + # Process images + process_images(scripts, assets_dir, output_dir) + + # Extract categories and tags + categories = get_categories(scripts) + all_tags = get_all_tags(scripts) + + # Generate HTML + html_content = generate_html(scripts, categories, all_tags) + + # Write index.html + index_path = output_dir / 'index.html' + with open(index_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"Site generated in {output_dir}/") + + +if __name__ == '__main__': + main()