diff --git a/.github/workflows/static.yml b/.github/workflows/deploy-site.yml
similarity index 55%
rename from .github/workflows/static.yml
rename to .github/workflows/deploy-site.yml
index 4396a4133..325cf041a 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/deploy-site.yml
@@ -1,5 +1,5 @@
# Simple workflow for deploying static content to GitHub Pages
-name: Deploy static content to Pages
+name: Deploy site to Pages
on:
# Runs on pushes targeting the default branch
@@ -33,6 +33,8 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
+ container:
+ image: registry.gitlab.com/kazam0180/pla-site-tool
steps:
- name: Checkout
uses: actions/checkout@v7
@@ -42,40 +44,48 @@ jobs:
uses: actions/cache@v6
with:
path: .cache/icons
- key: icons-${{ hashFiles('icons/**', 'no-icon.png', 'convert_icons') }}
+ key: icons-${{ hashFiles('icons/**', 'no-icon.png') }}
- name: Cache screenshots
id: screenshots-cache
uses: actions/cache@v6
with:
path: .cache/screenshots
- key: screenshots-${{ hashFiles('convert_screenshots') }}
+ key: screenshots-v1
- - name: Build App Files
+ - name: Build site
+ shell: bash
run: |
- sudo apt install --no-install-recommends wget esbuild -y
- wget -O pla-site-tool "$(curl -Ls https://api.github.com/repos/kazam0180/portable-apps/releases | sed 's/[()",{} ]/\n/g' | grep -oi "https.*pla-site-tool.*mage$" | head -1)"
-
- if [ ! -d .cache/icons ]; then
- wget -O magick "https://github.com/ImageMagick/ImageMagick/releases/download/7.1.2-29/ImageMagick-7.1.2-29-gcc-x86_64.AppImage"
- chmod a+x magick
- ./magick --appimage-extract
- ls squashfs-root
- sudo cp -r squashfs-root/usr/* /usr/
- sudo chmod a+x /usr/bin/magick
- rm -rf magick squashfs-root
- else
- echo "icon cache hit, skipping ImageMagick download" | tee build-log.txt
- fi
+ set -e
+
+ pla-site-tool > pla-site-tool.log 2>&1 &
+ PLA_PID=$!
+
+ imgtool -p . -o ./public convert-screenshots > screenshots.log 2>&1 &
+ SS_PID=$!
- [ ! -x ./pla-site-tool ] && chmod a+x ./pla-site-tool
- ./pla-site-tool 2>&1 | tee -a build-log.txt
- test ${PIPESTATUS[0]} -eq 0 || { echo "pla-site-tool failed, aborting build" | tee -a build-log.txt; exit 1; }
+ wait "$PLA_PID"; PLA_STATUS=$?
+ wait "$SS_PID"; SS_STATUS=$?
- rm -f sitemap.xml
- [ ! -x ./gen_sitemap ] && chmod a+x ./gen_sitemap
- ./gen_sitemap public https://portable-linux-apps.github.io/ 2>&1 | tee -a build-log.txt
+ cat pla-site-tool.log screenshots.log | tee -a build-log.txt
+
+ [ $PLA_STATUS -eq 0 ] || { echo "pla-site-tool failed" | tee -a build-log.txt; exit 1; }
+ [ $SS_STATUS -eq 0 ] || { echo "imgtool convert-screenshots failed" | tee -a build-log.txt; exit 1; }
+
+ [ ! -x ./minify ] && chmod a+x ./minify
+ ./minify public 2>&1 | tee -a build-log.txt
+
+ cp -t public/ favicon.ico *.webp *.jpg
+ cp -r assets/fonts public/assets/
+ cp -r casts public/
+ cp -r am-gui public/
+ cp -t public/ google*.html BingSiteAuth.xml
+
+ echo "site generated" | tee -a build-log.txt
+ - name: Process icons
+ run: |
+ set -e
if [ -d .cache/icons ]; then
mkdir -p public/icons public/icons_48
cp -r .cache/icons/public/icons/. public/icons/
@@ -84,8 +94,7 @@ jobs:
cp .cache/icons/public/no-icon_48.webp public/no-icon_48.webp
echo "icons restored from cache" | tee -a build-log.txt
else
- [ ! -x ./convert_icons ] && chmod a+x ./convert_icons
- ./convert_icons public 2>&1 | tee -a build-log.txt
+ imgtool -p . -o ./public convert-icons 2>&1 | tee -a build-log.txt
mkdir -p .cache/icons/public
cp -r public/icons .cache/icons/public/
cp -r public/icons_48 .cache/icons/public/
@@ -93,29 +102,6 @@ jobs:
cp public/no-icon_48.webp .cache/icons/public/
fi
- if ! command -v magick >/dev/null 2>&1; then
- wget -O magick "https://github.com/ImageMagick/ImageMagick/releases/download/7.1.2-29/ImageMagick-7.1.2-29-gcc-x86_64.AppImage"
- chmod a+x magick
- ./magick --appimage-extract
- sudo cp -r squashfs-root/usr/* /usr/
- sudo chmod a+x /usr/bin/magick
- rm -rf magick squashfs-root
- fi
-
- [ ! -x ./convert_screenshots ] && chmod a+x ./convert_screenshots
- ./convert_screenshots public 2>&1 | tee -a build-log.txt
-
- [ ! -x ./minify ] && chmod a+x ./minify
- ./minify public 2>&1 | tee -a build-log.txt
-
- cp -t public/ favicon.ico *.webp *.jpg
- cp -r assets/fonts public/assets/
- cp -r casts public/
- cp -r am-gui public/
- cp -t public/ sitemap.xml google*.html BingSiteAuth.xml
-
- echo "site generated" | tee -a build-log.txt
-
- name: Upload Build Log
uses: actions/upload-artifact@v7
with:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fe620a7ab..af6b2a8c7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -72,6 +72,7 @@ The site supports multiple languages using gettext `.po` files. Translatable str
**Directory structure:**
```
locales/
+├── pla-site.pot # Translation template
├── en/
│ └── pla-site.po # English translations (source language)
├── it/
@@ -122,7 +123,7 @@ The [static.yml](.github/workflows/static.yml) workflow builds and deploys the s
```sh
# download the generator
-wget -O pla-site-tool "https://github.com/kazam0180/portable-apps/releases/download/pla-site-tool-0.1.3/pla-site-tool_0.1.3-x86_64.AppImage"
+wget -O pla-site-tool "https://github.com/kazam0180/portable-apps/releases/download/pla-site-tool-0.2.3/pla-site-tool_0.2.3-x86_64.AppImage"
chmod +x pla-site-tool
./pla-site-tool # generates the site into public/
diff --git a/app_page.in b/app_page.in
index 154ab581f..b89d226bc 100644
--- a/app_page.in
+++ b/app_page.in
@@ -18,12 +18,12 @@
-
-
+
+
-
-
-
+
+
+
_("Skip to main content")
diff --git a/apps.in b/apps.in
index daf55d8a4..5c6c05692 100644
--- a/apps.in
+++ b/apps.in
@@ -1,5 +1,5 @@
-
+
@@ -15,11 +15,11 @@
-
-
+
+
-
-
+
+
Skip to main content
@@ -46,10 +46,8 @@
+
diff --git a/assets/css/style.css b/assets/css/style.css
index 8a35af4ef..2f0355389 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -192,6 +192,14 @@ nav a:focus {
background: var(--color-surface);
}
+/* === Results Count === */
+.results-count {
+ color: var(--color-text-muted);
+ font-size: 0.875rem;
+ margin-left: 0.5rem;
+ white-space: nowrap;
+}
+
@media (max-width: 640px) {
.lang-selector {
width: 100%;
diff --git a/assets/js/script.js b/assets/js/script.js
index 9bbd06edd..f934a1161 100644
--- a/assets/js/script.js
+++ b/assets/js/script.js
@@ -131,6 +131,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (visible) matched.push({ index: k, rank: terms.length ? matchRank(k, terms) : 0 });
}
+ var countEl = document.getElementById('results-count');
+ if (countEl) countEl.textContent = matched.length + ' results';
+
if (terms.length) {
matched.sort(function(a, b) {
return a.rank !== b.rank ? a.rank - b.rank : cache[a.index].name.localeCompare(cache[b.index].name);
diff --git a/cat_page.in b/cat_page.in
index 71f3e356f..e1cf7944a 100644
--- a/cat_page.in
+++ b/cat_page.in
@@ -1,5 +1,5 @@
-
+
@@ -17,11 +17,11 @@
-
-
+
+
-
-
+
+
_("Skip to main content")
@@ -48,10 +48,8 @@
+
diff --git a/convert_screenshots b/convert_screenshots
deleted file mode 100644
index 263987de3..000000000
--- a/convert_screenshots
+++ /dev/null
@@ -1,300 +0,0 @@
-#!/usr/bin/env python3
-
-import hashlib
-import json
-import os
-import re
-import shutil
-import subprocess
-import sys
-import threading
-import urllib.error
-import urllib.request
-from concurrent.futures import ThreadPoolExecutor
-from urllib.parse import urlparse
-
-SITE_DIR = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("SITE_DIR")
-if not SITE_DIR:
- print("Usage: convert_screenshots [repo_dir]", file=sys.stderr)
- sys.exit(1)
-REPO_DIR = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("REPO_DIR", ".")
-QUALITY = os.environ.get("WEBP_QUALITY", "100")
-
-APPS_DIR = os.path.join(REPO_DIR, "apps")
-CACHE_DIR = os.path.join(REPO_DIR, ".cache", "screenshots")
-DL_DIR = os.path.join(CACHE_DIR, "downloads")
-WEBP_DIR = os.path.join(CACHE_DIR, "webp")
-MANIFEST = os.path.join(CACHE_DIR, "manifest.json")
-OUT_DIR = os.path.join(SITE_DIR, "screenshots")
-
-MAX_BYTES = 64 * 1024 * 1024
-USER_AGENT = "Mozilla/5.0 (convert_screenshots; +https://portable-linux-apps.github.io)"
-EXTS = {".png": "png", ".jpg": "jpg", ".jpeg": "jpeg", ".gif": "gif", ".bmp": "bmp", ".webp": "webp"}
-
-
-def magick_cmd():
- for cmd in ("magick", "magick-im7.q16"):
- path = shutil.which(cmd)
- if path:
- return path
- print("Error: magick not found (install it with: sudo apt install imagemagick)", file=sys.stderr)
- sys.exit(1)
-
-
-MAGICK = magick_cmd()
-
-LOG_LOCK = threading.Lock()
-
-
-def log(msg):
- with LOG_LOCK:
- print(msg, file=sys.stderr, flush=True)
-
-
-def stable_key(url):
- return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
-
-
-def collect_urls():
- urls = {}
- for fname in sorted(os.listdir(APPS_DIR)):
- if fname.startswith(".") or fname.endswith("~"):
- continue
- path = os.path.join(APPS_DIR, fname)
- if not os.path.isfile(path):
- continue
- try:
- with open(path, "r", errors="replace") as fh:
- for line in fh:
- m = re.match(r"^#\s*SCREENSHOTS\s*:\s*(.+?)\s*$", line)
- if not m:
- continue
- for token in m.group(1).split():
- if not token.startswith(("http://", "https://")):
- continue
- path_only = token.split("?", 1)[0].split("#", 1)[0]
- if path_only.lower().endswith(".webp"):
- continue
- urls[token] = fname
- except OSError as e:
- print(f"warning: cannot read {path}: {e}")
- return urls
-
-
-def ext_for(url):
- path_only = url.split("?", 1)[0].split("#", 1)[0]
- ext = os.path.splitext(urlparse(path_only).path)[1].lower()
- ext = ext.split("?")[0]
- if ext in EXTS and ext != ".webp":
- return EXTS[ext]
- return "img"
-
-
-def read_manifest():
- if os.path.exists(MANIFEST):
- try:
- with open(MANIFEST, "r") as fh:
- return json.load(fh)
- except (OSError, ValueError):
- pass
- return {}
-
-
-def write_manifest(manifest):
- os.makedirs(CACHE_DIR, exist_ok=True)
- tmp = MANIFEST + ".tmp"
- with open(tmp, "w") as fh:
- json.dump(manifest, fh, indent=2, sort_keys=True)
- os.rename(tmp, MANIFEST)
-
-
-def fetch(url, etag=None):
- req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
- if etag:
- req.add_header("If-None-Match", etag)
- try:
- with urllib.request.urlopen(req, timeout=60) as resp:
- if resp.status == 304:
- return "unchanged", "", {}
- headers = dict(resp.headers.items())
- buf = bytearray()
- while True:
- chunk = resp.read(65536)
- if not chunk:
- break
- buf.extend(chunk)
- if len(buf) > MAX_BYTES:
- return "error", "", {}
- return "ok", bytes(buf), headers
- except urllib.error.HTTPError as e:
- if e.code == 304:
- return "unchanged", "", {}
- if e.code == 404:
- return "missing", "", {}
- return "error", "", {}
- except OSError:
- return "error", "", ""
-
-
-def is_image_body(body):
- return (body[:8] == b"\x89PNG\r\n\x1a\n"
- or body[:3] == b"\xff\xd8\xff"
- or body[:6] in (b"GIF87a", b"GIF89a")
- or (body[:4] == b"RIFF" and body[8:12] == b"WEBP"))
-
-
-def convert_to_webp(src_path, dst_path):
- tmp = dst_path + ".tmp"
- try:
- os.makedirs(os.path.dirname(dst_path), exist_ok=True)
- subprocess.run([MAGICK, "-quality", QUALITY, src_path, tmp],
- check=True, capture_output=True, timeout=120)
- os.rename(tmp, dst_path)
- return True
- except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
- if os.path.exists(tmp):
- os.unlink(tmp)
- return False
-
-
-def process_url(url, app, manifest, stats):
- log(f"[{app}] processing {url}")
- key = stable_key(url)
- entry = manifest.get(url)
- dl_file = os.path.join(DL_DIR, f"{key}.{ext_for(url)}")
- webp_file = os.path.join(WEBP_DIR, f"{key}.webp")
-
- if entry and os.path.exists(webp_file):
- if entry.get("etag"):
- status, body, headers = fetch(url, etag=entry["etag"])
- if status == "unchanged":
- stats["unchanged"] += 1
- return key
- if status == "missing":
- stats["missing"] += 1
- manifest.pop(url, None)
- return None
- if status != "ok":
- stats["changed"] += 1
- manifest.pop(url, None)
- return None
- body = None
- else:
- stats["unchanged"] += 1
- return key
- else:
- status, body, headers = fetch(url)
- if status == "missing":
- stats["missing"] += 1
- return None
- if status != "ok":
- stats["failed"] += 1
- return None
- manifest.pop(url, None)
- stats["downloaded"] += 1
- if not is_image_body(body):
- stats["failed"] += 1
- return None
-
- sha = hashlib.sha256(body if body is not None else b"").hexdigest()
- if body is not None:
- os.makedirs(DL_DIR, exist_ok=True)
- tmp = dl_file + ".tmp"
- with open(tmp, "wb") as fh:
- fh.write(body)
- os.rename(tmp, dl_file)
-
- if body is not None and body[:12] == b"RIFF" and b"WEBP" in body[:16]:
- shutil.copy2(dl_file, webp_file)
- else:
- if not convert_to_webp(dl_file, webp_file):
- stats["failed"] += 1
- return None
- stats["converted"] += 1
-
- manifest[url] = {
- "file": os.path.basename(dl_file),
- "webp": f"{key}.webp",
- "sha256": sha,
- "etag": headers.get("ETag", headers.get("etag")) if isinstance(headers, dict) else None,
- }
- return key
-
-
-def replace_in_file(path, mapping, rel_prefix):
- try:
- with open(path, "r", encoding="utf-8", errors="replace") as fh:
- content = fh.read()
- except OSError:
- return 0
- count = 0
- for url, key in mapping.items():
- new = f"/screenshots/{key}.webp"
- n = content.count(url)
- if n:
- content = content.replace(url, new)
- count += n
- if count:
- tmp = path + ".tmp"
- with open(tmp, "w", encoding="utf-8") as fh:
- fh.write(content)
- os.rename(tmp, path)
- return count
-
-
-def main():
- urls = collect_urls()
- if not urls:
- print("no screenshot URLs found in app files")
- return
-
- manifest = read_manifest()
- if os.path.exists(MANIFEST):
- log(f"cache: found ({len(manifest)} entries)")
- else:
- log("cache: not found (fresh run)")
- stats = {"downloaded": 0, "converted": 0, "unchanged": 0,
- "missing": 0, "failed": 0, "changed": 0}
- results = {}
-
- with ThreadPoolExecutor(max_workers=8) as pool:
- futures = {pool.submit(process_url, url, urls[url], manifest, stats): url for url in urls}
- for fut in futures:
- url = futures[fut]
- key = fut.result()
- if key:
- results[url] = key
-
- for url in list(manifest):
- if url not in urls:
- del manifest[url]
-
- write_manifest(manifest)
-
- if os.path.isdir(OUT_DIR):
- shutil.rmtree(OUT_DIR)
- os.makedirs(OUT_DIR, exist_ok=True)
- os.makedirs(WEBP_DIR, exist_ok=True)
- copied = 0
- if os.path.isdir(WEBP_DIR):
- for webp in os.listdir(WEBP_DIR):
- if webp.endswith(".webp") and os.path.isfile(os.path.join(WEBP_DIR, webp)):
- shutil.copy2(os.path.join(WEBP_DIR, webp), os.path.join(OUT_DIR, webp))
- copied += 1
-
- replaced_total = 0
- for root, dirs, files in os.walk(SITE_DIR):
- dirs[:] = [d for d in dirs if d not in (".cache",)]
- for fname in files:
- if not fname.endswith((".json", ".html")):
- continue
- replaced_total += replace_in_file(os.path.join(root, fname), results)
-
- print(f"screenshots: {stats['downloaded']} downloaded, {stats['converted']} converted, "
- f"{stats['unchanged']} unchanged (cache), {stats['missing']} not found, "
- f"{stats['failed']} failed, {copied} webp in {OUT_DIR}, "
- f"{replaced_total} URL references replaced")
-
-
-if __name__ == "__main__":
- main()
diff --git a/index.in b/index.in
index 8ec2b668c..51f02617b 100644
--- a/index.in
+++ b/index.in
@@ -1,5 +1,5 @@
-
+
@@ -17,11 +17,11 @@
-
-
-
-
-
+
+
+
+
+
_("Skip to main content")
@@ -59,9 +59,6 @@