Browse Source

Merge pull request #2507 from element-hq/feature/bma/generateWorldScreenshots

Generate world screenshots and publish them as GitHub pages.
pull/2508/head
Benoit Marty 7 months ago committed by GitHub
parent
commit
bfc65c5e70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 35
      .github/workflows/generate_github_pages.yml
  2. 2697
      screenshots/html/data.js
  3. 9
      screenshots/html/screenshots.css
  4. 114
      screenshots/html/script.js
  5. 2
      screenshots/index.html
  6. 79
      tools/test/generateAllScreenshots.py
  7. 40
      tools/test/generateWorldScreenshots.py

35
.github/workflows/generate_github_pages.yml

@ -0,0 +1,35 @@
name: Generate GitHub Pages
on:
workflow_dispatch:
schedule:
# At 00:00 on every Tuesday UTC
- cron: '0 0 * * 2'
jobs:
generate-github-pages:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'element-hq/element-x-android'
steps:
- uses: actions/checkout@v4
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Configure gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Run World screenshots generation script
run: |
./tools/test/generateWorldScreenshots.py
- name: Deploy GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./screenshots

2697
screenshots/html/data.js

File diff suppressed because it is too large Load Diff

9
screenshots/html/screenshots.css

@ -50,10 +50,19 @@ img {
display: block; display: block;
} }
.missing {
text-align: center;
}
th { th {
background: #0DBD8B; background: #0DBD8B;
} }
br {
display: block;
margin: 4px 0;
}
#form_container { #form_container {
margin: 0px auto; margin: 0px auto;
} }

114
screenshots/html/script.js

@ -15,10 +15,19 @@
*/ */
import { screenshots } from './data.js'; import { screenshots } from './data.js';
const header = screenshots[0]; const dataLanguages = screenshots[0];
const dataPaths = screenshots[1];
// Read default visible languages from the fragment
const fragment = new URLSearchParams(window.location.hash.substring(1));
// Get the wanted languages from the fragment, or default to "de" and "fr", and ensure "en" is always there
const wantedLanguages = (fragment.get('languages') ? fragment.get('languages').split(',') : ['de', 'fr']) + ["en"];
// Map dataLanguages to visibleLanguages, set to 1 if the language is in wantedLanguages, 0 otherwise
let visibleLanguages = dataLanguages.map((language) => wantedLanguages.includes(language) ? 1 : 0);
let visibleLanguages = header.map((x) => 1);
let imageWidth = 300; let imageWidth = 300;
let showAllScreenshots = false;
function addForm() { function addForm() {
// Insert the form into the div with id form_container // Insert the form into the div with id form_container
@ -26,14 +35,14 @@ function addForm() {
const languageLabel = document.createElement('label'); const languageLabel = document.createElement('label');
languageLabel.textContent = 'Languages:'; languageLabel.textContent = 'Languages:';
form.appendChild(languageLabel); form.appendChild(languageLabel);
// Add a check box per entry in the header // Add a check box per entry in the dataLanguages
for (let i = 0; i < header.length; i++){ for (let i = 0; i < dataLanguages.length; i++){
const label = document.createElement('label'); const label = document.createElement('label');
const text = document.createTextNode(header[i]); const text = document.createTextNode(dataLanguages[i]);
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'checkbox'; input.type = 'checkbox';
input.disabled = i == 0; input.disabled = i == 0;
input.name = header[i]; input.name = dataLanguages[i];
input.checked = visibleLanguages[i] == 1; input.checked = visibleLanguages[i] == 1;
input.onchange = (e) => { input.onchange = (e) => {
if (e.target.checked) { if (e.target.checked) {
@ -47,6 +56,8 @@ function addForm() {
label.appendChild(text); label.appendChild(text);
form.appendChild(label); form.appendChild(label);
} }
// Add a break line
form.appendChild(document.createElement('br'));
// Add a label with the text "Width" // Add a label with the text "Width"
const label = document.createElement('label'); const label = document.createElement('label');
label.textContent = 'Screenshots width:'; label.textContent = 'Screenshots width:';
@ -64,6 +75,20 @@ function addForm() {
addTable(); addTable();
}; };
form.appendChild(widthInput); form.appendChild(widthInput);
// Add a label with the text "Show all screenshots"
const label2 = document.createElement('label');
label2.textContent = 'Show all screenshots:';
label2.title = 'Show all screenshots, including those with no translated versions.';
const input2 = document.createElement('input');
input2.type = 'checkbox';
input2.name = "test";
input2.checked = showAllScreenshots;
input2.onchange = (e) => {
showAllScreenshots = e.target.checked;
addTable();
};
label2.appendChild(input2);
form.appendChild(label2);
document.getElementById('form_container').appendChild(form); document.getElementById('form_container').appendChild(form);
} }
@ -86,55 +111,76 @@ function addTable() {
// First item of screenshots contains the languages // First item of screenshots contains the languages
// Build the languages row // Build the languages row
const languagesHeaderRow = document.createElement('tr'); const languagesHeaderRow = document.createElement('tr');
for (let i = 0; i < header.length; i++) { for (let languageIndex = 0; languageIndex < dataLanguages.length; languageIndex++) {
// Do not add the language if it is hidden // Do not add the language if it is hidden
if (visibleLanguages[i] == 0) { if (visibleLanguages[languageIndex] == 0) {
continue; continue;
} }
const th = document.createElement('th'); const th = document.createElement('th');
th.textContent = header[i]; th.textContent = dataLanguages[languageIndex];
languagesHeaderRow.appendChild(th); languagesHeaderRow.appendChild(th);
} }
const numVisibleLanguages = languagesHeaderRow.childElementCount const numVisibleLanguages = languagesHeaderRow.childElementCount
// Second item contains the paths
// Next items are the data // Next items are the data
var currentHeaderValue = ""; var currentHeaderValue = "";
for (let i = 1; i < screenshots.length; i++) { for (let screenshotIndex = 2; screenshotIndex < screenshots.length; screenshotIndex++) {
// Add a header for row, if different from previous let englishFile = screenshots[screenshotIndex][0];
let name = getNiceName(screenshots[i][0]);
if(name != currentHeaderValue) {
currentHeaderValue = name;
const trHead = document.createElement('tr');
const tdHead = document.createElement('td');
tdHead.colSpan = numVisibleLanguages;
tdHead.className = "view-header";
tdHead.textContent = name;
trHead.appendChild(tdHead);
tbody.appendChild(trHead);
tbody.appendChild(languagesHeaderRow.cloneNode(true));
}
const tr = document.createElement('tr'); const tr = document.createElement('tr');
for (let j = 0; j < screenshots[i].length; j++) { let hasTranslatedFiles = false;
if (visibleLanguages[j] == 0) { for (let languageIndex = 0; languageIndex < dataLanguages.length; languageIndex++) {
if (visibleLanguages[languageIndex] == 0) {
continue; continue;
} }
const td = document.createElement('td'); const td = document.createElement('td');
let imageFile = screenshots[i][j]; if (languageIndex == 0) {
if (imageFile === '') { const fullFile = `${dataPaths[0]}/${englishFile}.png`;
const text = document.createElement('p');
text.textContent = 'No image';
td.appendChild(text);
} else {
const img = document.createElement('img'); const img = document.createElement('img');
img.className = "screenshot"; img.className = "screenshot";
img.src = `../${imageFile}`; img.src = `../${fullFile}`;
img.title = imageFile; img.title = fullFile;
img.alt = "Missing image"; img.alt = "Missing image";
img.width = imageWidth; img.width = imageWidth;
td.appendChild(img); td.appendChild(img);
} else {
let hasFile = screenshots[screenshotIndex][languageIndex];
if (hasFile === 0) {
const text = document.createElement('p');
text.className = "missing";
text.textContent = 'No image';
td.appendChild(text);
} else {
hasTranslatedFiles = true;
// Foreign file is the same as the english file, replacing the language
const foreignFile = englishFile.replace("en]", `${dataLanguages[languageIndex]}]`).replace("_S_", "_T_")
const fullForeignFile = `${dataPaths[languageIndex]}/${foreignFile}.png`;
const img = document.createElement('img');
img.className = "screenshot";
img.src = `../${fullForeignFile}`;
img.title = fullForeignFile;
img.alt = "Missing image";
img.width = imageWidth;
td.appendChild(img);
}
} }
tr.appendChild(td); tr.appendChild(td);
} }
tbody.appendChild(tr); if (showAllScreenshots || hasTranslatedFiles) {
// Add a header for row, if different from previous
let name = getNiceName(englishFile);
if (name != currentHeaderValue) {
currentHeaderValue = name;
const trHead = document.createElement('tr');
const tdHead = document.createElement('td');
tdHead.colSpan = numVisibleLanguages;
tdHead.className = "view-header";
tdHead.textContent = name;
trHead.appendChild(tdHead);
tbody.appendChild(trHead);
tbody.appendChild(languagesHeaderRow.cloneNode(true));
}
tbody.appendChild(tr);
}
} }
table.appendChild(thead); table.appendChild(thead);
table.appendChild(tbody); table.appendChild(tbody);

2
screenshots/index.html

@ -19,7 +19,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Element X Android Gallery</title> <title>Element X Android Gallery</title>
<link rel="icon" href="../.idea/icon.png"> <link rel="icon" href="https://raw.githubusercontent.com/element-hq/element-x-android/develop/.idea/icon.png">
<link rel="stylesheet" href="html/screenshots.css"> <link rel="stylesheet" href="html/screenshots.css">
</head> </head>
<!-- load script which is in ./html/script.js --> <!-- load script which is in ./html/script.js -->

79
tools/test/generateAllScreenshots.py

@ -1,35 +1,52 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#
# Copyright 2024 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os import os
import re import re
import sys import sys
from util import compare from util import compare
# Read all arguments and return a list of them, this are the languages list. # Read all arguments and return a list of them, this are the languages list.
def readArguments(): def readArguments():
# Return sys.argv without the first argument # Return sys.argv without the first argument
return sys.argv[1:] return sys.argv[1:]
def generateAllScreenshots(languages): def generateAllScreenshots(languages):
# If languages is empty, generate all screenshots # If languages is empty, generate all screenshots
if len(languages) == 0: if len(languages) == 0:
print("Generating all screenshots...") print("Generating all screenshots...")
os.system("./gradlew recordPaparazziDebug -PallLanguages") os.system("./gradlew recordPaparazziDebug -PallLanguages")
else: else:
tFile = "tests/uitests/src/test/kotlin/ui/T.kt" tFile = "tests/uitests/src/test/kotlin/ui/T.kt"
print("Generating screenshots for languages: %s" % languages) print("Generating screenshots for languages: %s" % languages)
# Patch file T.kt, replace `@TestParameter(value = ["de"]) localeStr: String,` with `@TestParameter(value = ["de", "fr"]) localeStr: String,` # Record the languages one by one, else it's getting too slow
with open(tFile, "r") as file: for lang in languages:
data = file.read() print("Generating screenshots for language: %s" % lang)
languagesList = ", ".join([f"\"{lang}\"" for lang in languages]) # Patch file T.kt, replace `@TestParameter(value = ["de"]) localeStr: String,` with `@TestParameter(value = [<the languages>]) localeStr: String,`
data = data.replace("@TestParameter(value = [\"de\"]) localeStr: String,", "@TestParameter(value = [%s]) localeStr: String," % languagesList) with open(tFile, "r") as file:
with open(tFile, "w") as file: data = file.read()
file.write(data) data = data.replace("@TestParameter(value = [\"de\"]) localeStr: String,", "@TestParameter(value = [\"%s\"]) localeStr: String," % lang)
os.system("./gradlew recordPaparazziDebug -PallLanguagesNoEnglish") with open(tFile, "w") as file:
# Git reset the change on file T.kt file.write(data)
os.system("git checkout HEAD -- %s" % tFile) os.system("./gradlew recordPaparazziDebug -PallLanguagesNoEnglish")
# Git reset the change on file T.kt
os.system("git checkout HEAD -- %s" % tFile)
def detectLanguages(): def detectLanguages():
@ -82,38 +99,44 @@ def detectRecordedLanguages():
# List all the subfolders of the screenshots folder which contains 2 letters, sorted alphabetically # List all the subfolders of the screenshots folder which contains 2 letters, sorted alphabetically
return sorted([f for f in os.listdir("screenshots") if len(f) == 2]) return sorted([f for f in os.listdir("screenshots") if len(f) == 2])
def generateJavascriptFile(): def generateJavascriptFile():
__doc__ = "Generate a javascript file to load the screenshots" __doc__ = "Generate a javascript file to load the screenshots"
print("Generating javascript file...") print("Generating javascript file...")
languages = detectRecordedLanguages() languages = detectRecordedLanguages()
# First item is the list of languages, adding "en" at the beginning # First item is the list of languages, adding "en" at the beginning
data = [["en"] + languages] data = [["en"] + languages]
# If any translated screenshot exists, keep the file # Second item is the path of the containing file
data.append(["./tests/uitests/src/test/snapshots/images"] + ["./screenshots/" + l for l in languages])
files = sorted( files = sorted(
os.listdir("tests/uitests/src/test/snapshots/images/"), os.listdir("tests/uitests/src/test/snapshots/images/"),
key=lambda file: file[file.find("_", 6):], key=lambda file: file[file.find("_", 6):],
) )
for file in files: for file in files:
fullFile = "./tests/uitests/src/test/snapshots/images/" + file # Continue if file contains "-Night", keep only light screenshots (maybe the night screenshots could be on the second column?)
dataForFile = [fullFile] if "-Night" in file:
hasAnyTranslatedFile = False continue
dataForFile = [file[:-4]]
for l in languages: for l in languages:
translatedFile = "./screenshots/" + l + "/" + file[:3] + "T" + file[4:-7] + l + file[-5:] simpleFile = file[:3] + "T" + file[4:-7] + l + file[-5:-4]
translatedFile = "./screenshots/" + l + "/" + simpleFile + ".png"
if os.path.exists(translatedFile): if os.path.exists(translatedFile):
hasAnyTranslatedFile = True dataForFile.append(1)
dataForFile.append(translatedFile)
else: else:
dataForFile.append("") dataForFile.append(0)
if hasAnyTranslatedFile: data.append(dataForFile)
data.append(dataForFile)
with open("screenshots/html/data.js", "w") as f: with open("screenshots/html/data.js", "w") as f:
f.write("// Generated file, do not edit\n") f.write("// Generated file, do not edit\n")
f.write("export const screenshots = [\n") f.write("export const screenshots = [\n")
for line in data: for line in data:
f.write("[\n") f.write("[")
for item in line: for item in line:
f.write("\"" + item + "\",\n") # If item is a string, add quotes
if isinstance(item, str):
f.write("\"" + item + "\",")
else:
f.write(str(item) + ",")
f.write("],\n") f.write("],\n")
f.write("];\n") f.write("];\n")

40
tools/test/generateWorldScreenshots.py

@ -0,0 +1,40 @@
#!/usr/bin/env python3
#
# Copyright 2024 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
def detectAllExistingTranslations():
# Read all the folder in "libraries/ui-strings/src/main/res"
folders = os.listdir("libraries/ui-strings/src/main/res")
# Remove the "values" folder
folders.remove("values")
# Map to keep only the language code
folders = list(map(lambda folder: folder[7:], folders))
# Map to keep only the string before the "-"
folders = list(map(lambda folder: folder.split("-")[0], folders))
# Remove duplicates
folders = list(set(folders))
return folders
def main():
languages = detectAllExistingTranslations()
print ("Will record the screenshots for those languages: %s" % languages)
# Run the python script "generateAllScreenshots.py" with the detected languages
os.system("./tools/test/generateAllScreenshots.py %s" % " ".join(languages))
main()
Loading…
Cancel
Save