diff --git a/.github/social-preview-nemo.png b/.github/social-preview-nemo.png new file mode 100644 index 0000000..1a58085 Binary files /dev/null and b/.github/social-preview-nemo.png differ diff --git a/.github/social-preview-src/README.md b/.github/social-preview-src/README.md new file mode 100644 index 0000000..4fe22cb --- /dev/null +++ b/.github/social-preview-src/README.md @@ -0,0 +1,38 @@ +# Social preview source + +The repository preview is rendered at 1280×640 from three lossless sources: + +- `background.png`: the original 1774×887 abstract event-horizon artwork. +- the canonical PDrive SVG icon under `share/icons/`. +- deterministic SVG typography and layout generated by `render.py`. +- output paths and distro or file-manager copy from `variants.json`. + +The committed outputs are: + +- `.github/social-preview.png`: distro-neutral GitHub repository preview. +- `.github/social-preview-nemo.png`: preserved Nemo-specific store artwork. + +Render both variants with: + +```console +make social-preview +``` + +Verify that the committed PNGs still match their sources with: + +```console +make check-social-preview +``` + +Both commands process every profile in `variants.json`. Add another file-manager +or distribution profile there, including its repository-relative output path; +the background, icon, typography, and layout remain shared, and the new variant +automatically joins both the build and freshness check. + +The background was generated specifically for PDrive with this prompt: + +> Create an elegant abstract open-source technology backdrop suggesting a calm +> event horizon, secure cloud storage, and flowing data paths. Use deep +> near-black navy, indigo, restrained violet, cyan, and mint. Keep the left side +> dark and quiet for branding, and place a gentle luminous arc on the right. No +> text, letters, logos, icons, screenshots, UI, people, or watermark. diff --git a/.github/social-preview-src/background.png b/.github/social-preview-src/background.png new file mode 100644 index 0000000..f73e25a Binary files /dev/null and b/.github/social-preview-src/background.png differ diff --git a/.github/social-preview-src/overlay.svg b/.github/social-preview-src/overlay.svg new file mode 100644 index 0000000..1078501 --- /dev/null +++ b/.github/social-preview-src/overlay.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + OSS SINGULARITY / OPEN SOURCE + + + PDrive + Control Center + + Proton Drive, at home on Linux. + {{TAGLINE}} + + + + {{BADGE}} + + RESILIENT UPLOADS + + LIVE INSIGHT + + + github.com/oss-singularity/proton-drive-linux + diff --git a/.github/social-preview-src/render-all.py b/.github/social-preview-src/render-all.py new file mode 100755 index 0000000..ab807a5 --- /dev/null +++ b/.github/social-preview-src/render-all.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Render or verify every configured PDrive social-preview variant.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import tempfile +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="fail if a committed preview differs from a fresh render", + ) + return parser.parse_args() + + +def configured_output(repo_dir: Path, value: object) -> Path: + relative = Path(str(value)) + if relative.is_absolute(): + raise SystemExit(f"Preview output must be repository-relative: {relative}") + + output = (repo_dir / relative).resolve() + try: + output.relative_to(repo_dir) + except ValueError as error: + raise SystemExit(f"Preview output escapes the repository: {relative}") from error + return output + + +def render(render_script: Path, variant: str, output: Path) -> None: + subprocess.run( + [ + sys.executable, + str(render_script), + "--variant", + variant, + "--output", + str(output), + ], + check=True, + ) + + +def main() -> int: + args = parse_args() + source_dir = Path(__file__).resolve().parent + repo_dir = source_dir.parents[1].resolve() + render_script = source_dir / "render.py" + variants = json.loads((source_dir / "variants.json").read_text(encoding="utf-8")) + + if not isinstance(variants, dict) or not variants: + raise SystemExit("At least one social-preview variant must be configured.") + + if not args.check: + for name in sorted(variants): + output = configured_output(repo_dir, variants[name]["output"]) + render(render_script, name, output) + return 0 + + stale: list[Path] = [] + with tempfile.TemporaryDirectory(prefix="pdrive-social-preview-check-") as temporary: + check_dir = Path(temporary) + for name in sorted(variants): + committed = configured_output(repo_dir, variants[name]["output"]) + candidate = check_dir / f"{name}.png" + render(render_script, name, candidate) + if not committed.is_file() or committed.read_bytes() != candidate.read_bytes(): + stale.append(committed) + + if stale: + for output in stale: + print(f"Stale social preview: {output.relative_to(repo_dir)}", file=sys.stderr) + print("Run make social-preview and commit the results.", file=sys.stderr) + return 1 + + print(f"All {len(variants)} social-preview variants are current.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/social-preview-src/render.py b/.github/social-preview-src/render.py new file mode 100755 index 0000000..2f37e33 --- /dev/null +++ b/.github/social-preview-src/render.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Render a deterministic PDrive social-preview variant.""" + +from __future__ import annotations + +import argparse +import html +import json +import shutil +import subprocess +import tempfile +from pathlib import Path + +WIDTH = 1280 +HEIGHT = 640 + + +def parse_args(variants: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variant", choices=variants, required=True) + parser.add_argument("--output", type=Path, required=True) + return parser.parse_args() + + +def image_magick() -> list[str]: + executable = shutil.which("magick") or shutil.which("convert") + if not executable: + raise SystemExit("ImageMagick is required (magick or convert).") + return [executable] + + +def main() -> int: + source_dir = Path(__file__).resolve().parent + repo_dir = source_dir.parents[1] + variants = json.loads((source_dir / "variants.json").read_text(encoding="utf-8")) + args = parse_args(sorted(variants)) + variant = variants[args.variant] + + background = source_dir / "background.png" + icon = repo_dir / "share/icons/hicolor/scalable/apps/io.github.claudiuschuster.PDriveControl.svg" + if not background.is_file() or not icon.is_file(): + raise SystemExit("Social-preview background or PDrive icon is missing.") + + badge_width = int(variant["badge_width"]) + second_x = 76 + badge_width + 13 + third_x = second_x + 208 + 13 + replacements = { + "{{TAGLINE}}": html.escape(str(variant["tagline"])), + "{{BADGE}}": html.escape(str(variant["badge"])), + "{{BADGE_WIDTH}}": str(badge_width), + "{{BADGE_CENTER}}": str(76 + badge_width / 2), + "{{SECOND_X}}": str(second_x), + "{{SECOND_CENTER}}": str(second_x + 104), + "{{THIRD_X}}": str(third_x), + "{{THIRD_CENTER}}": str(third_x + 75), + } + overlay_text = (source_dir / "overlay.svg").read_text(encoding="utf-8") + for marker, value in replacements.items(): + overlay_text = overlay_text.replace(marker, value) + + command = image_magick() + output = args.output.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="pdrive-social-preview-") as temporary: + work_dir = Path(temporary) + overlay_svg = work_dir / "overlay.svg" + overlay_png = work_dir / "overlay.png" + canvas = work_dir / "background.png" + icon_source = work_dir / "icon-source.png" + app_icon = work_dir / "icon.png" + overlay_svg.write_text(overlay_text, encoding="utf-8") + + subprocess.run( + command + ["-background", "none", str(overlay_svg), str(overlay_png)], + check=True, + ) + subprocess.run( + command + [str(background), "-resize", f"{WIDTH}x{HEIGHT}!", str(canvas)], + check=True, + ) + subprocess.run( + command + + [ + "-background", + "none", + str(icon), + "-resize", + "256x256", + str(icon_source), + ], + check=True, + ) + subprocess.run( + command + [str(icon_source), "-resize", "142x142", str(app_icon)], + check=True, + ) + subprocess.run( + command + + [ + str(canvas), + str(overlay_png), + "-composite", + str(app_icon), + "-geometry", + "+76+155", + "-composite", + "-strip", + str(output), + ], + check=True, + ) + + print(f"Rendered {args.variant}: {output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/social-preview-src/variants.json b/.github/social-preview-src/variants.json new file mode 100644 index 0000000..5c89c22 --- /dev/null +++ b/.github/social-preview-src/variants.json @@ -0,0 +1,14 @@ +{ + "generic": { + "output": ".github/social-preview.png", + "tagline": "Native file-explorer access. Resilient uploads. Clear live insight.", + "badge": "FILE EXPLORER", + "badge_width": 178 + }, + "nemo": { + "output": ".github/social-preview-nemo.png", + "tagline": "Native Nemo access. Resilient uploads. Clear live insight.", + "badge": "NEMO NATIVE", + "badge_width": 158 + } +} diff --git a/.github/social-preview.png b/.github/social-preview.png new file mode 100644 index 0000000..801e9e9 Binary files /dev/null and b/.github/social-preview.png differ diff --git a/Makefile b/Makefile index 7597ed4..76bee6d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL := /bin/bash .DEFAULT_GOAL := help -.PHONY: help version check check-units check-display verify install install-with-proton-cli uninstall +.PHONY: help version check check-units check-display verify social-preview check-social-preview install install-with-proton-cli uninstall help: ## Show this action-free command overview. @printf '%s\n' \ @@ -11,6 +11,8 @@ help: ## Show this action-free command overview. ' make check-units Validate systemd user units only.' \ ' make check-display Run GTK tests on the current display.' \ ' make verify Run diff hygiene and the portable suite.' \ + ' make social-preview Render every configured social-preview variant.' \ + ' make check-social-preview Verify committed previews match their sources.' \ ' make version Print the project version.' \ ' make install Install or update user-local files.' \ ' make install-with-proton-cli Also enable the optional Proton CLI updater.' \ @@ -33,6 +35,12 @@ verify: ## Check diff hygiene and run the portable repository suite. git diff --check $(MAKE) check +social-preview: ## Render every configured social-preview variant. + python3 .github/social-preview-src/render-all.py + +check-social-preview: ## Verify committed previews match their sources. + python3 .github/social-preview-src/render-all.py --check + install: ## Install or update user-local project files. ./install.sh diff --git a/tests/check.sh b/tests/check.sh index 7688a37..2923dd4 100755 --- a/tests/check.sh +++ b/tests/check.sh @@ -36,7 +36,9 @@ done for python_file in "${project_dir}/bin/pdrive-state" "${project_dir}/bin/pdrive-ui" \ "${project_dir}/libexec/pdrive-draft-recovery-auto" \ - "${project_dir}/tests/test-draft-recovery.py"; do + "${project_dir}/tests/test-draft-recovery.py" \ + "${project_dir}/.github/social-preview-src/render.py" \ + "${project_dir}/.github/social-preview-src/render-all.py"; do python3 - "${python_file}" <<'PY' import pathlib import sys diff --git a/tests/test-help.sh b/tests/test-help.sh index f8f07f4..65c365e 100755 --- a/tests/test-help.sh +++ b/tests/test-help.sh @@ -55,6 +55,8 @@ HOME="${test_home}" bash "${project_dir}/libexec/reauth-rclone-proton" --help >/ HOME="${test_home}" "${project_dir}/libexec/pdrive-auth-failure-guard" >/dev/null HOME="${test_home}" "${project_dir}/libexec/pdrive-auth-failure-guard" --help >/dev/null HOME="${test_home}" make -s -C "${project_dir}" help >/dev/null +HOME="${test_home}" python3 "${project_dir}/.github/social-preview-src/render.py" --help >/dev/null +HOME="${test_home}" python3 "${project_dir}/.github/social-preview-src/render-all.py" --help >/dev/null HOME="${test_home}" make -s -C "${project_dir}" version >/dev/null after="$(snapshot)"