Add script to generate Play Store screenshot sizes
- Introduced a Node.js script (`playstore-screenshot-sizes.mjs`) to resize images for Play Store screenshots based on predefined profiles (phone, tablet-7, tablet-10). - The script reads images from a specified input directory, processes them, and saves the resized images in an output directory with appropriate naming conventions. - Added a Bash wrapper script (`playstore-screenshot-sizes.sh`) to execute the Node.js script easily from the command line.
@@ -6,6 +6,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -118,9 +119,8 @@ jobs:
|
|||||||
./osv-scanner --lockfile ./package-lock.json
|
./osv-scanner --lockfile ./package-lock.json
|
||||||
|
|
||||||
deploy-production:
|
deploy-production:
|
||||||
needs: analyze
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare SSH
|
- name: Prepare SSH
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -36,13 +36,12 @@ Ausgabe in:
|
|||||||
|
|
||||||
## 3) Screenshots (anonymisiert)
|
## 3) Screenshots (anonymisiert)
|
||||||
|
|
||||||
### Grobe Anforderungen (Telefon)
|
### Zielgroessen fuer Store-Upload
|
||||||
- Mindestens 2 Screenshots
|
- Telefon (Portrait): 1080 x 1920
|
||||||
- PNG oder JPEG
|
- Medium 7" Tablet (Portrait): 1200 x 1920
|
||||||
- Seitenlaenge je Seite zwischen 320 px und 3840 px
|
- 10" Tablet (Portrait): 1600 x 2560
|
||||||
|
|
||||||
Empfehlung fuer Android-Phone:
|
Alle Dateien als PNG oder JPEG.
|
||||||
- 1080 x 1920 (Portrait)
|
|
||||||
|
|
||||||
### Anonymisierung
|
### Anonymisierung
|
||||||
|
|
||||||
@@ -61,10 +60,33 @@ Beispiel:
|
|||||||
'68,118,520,72;70,706,560,98'
|
'68,118,520,72;70,706,560,98'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Zielprofile erzeugen (Telefon, 7", 10")
|
||||||
|
|
||||||
|
Aus allen Dateien in `android-app/playstore-assets/anon` werden die drei Profile erzeugt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/playstore-screenshot-sizes.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional mit eigenen Ordnern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/playstore-screenshot-sizes.sh \
|
||||||
|
--input-dir android-app/playstore-assets/anon \
|
||||||
|
--output-dir android-app/playstore-assets/final
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- android-app/playstore-assets/final/phone
|
||||||
|
- android-app/playstore-assets/final/tablet-7
|
||||||
|
- android-app/playstore-assets/final/tablet-10
|
||||||
|
|
||||||
## 4) Upload in Play Console
|
## 4) Upload in Play Console
|
||||||
|
|
||||||
- Datenschutzerklaerung: URL eintragen
|
- Datenschutzerklaerung: URL eintragen
|
||||||
- Konto-Loeschung: URL eintragen
|
- Konto-Loeschung: URL eintragen
|
||||||
- App-Icon: playstore-icon-512.png
|
- App-Icon: playstore-icon-512.png
|
||||||
- Feature Graphic: playstore-feature-graphic-1024x500.png
|
- Feature Graphic: playstore-feature-graphic-1024x500.png
|
||||||
- Screenshots: anonymisierte PNG/JPEG hochladen
|
- Screenshots Telefon: Dateien aus `.../final/phone`
|
||||||
|
- Screenshots 7" Tablet: Dateien aus `.../final/tablet-7`
|
||||||
|
- Screenshots 10" Tablet: Dateien aus `.../final/tablet-10`
|
||||||
|
|||||||
BIN
android-app/playstore-assets/anon/Screenshot_20260530_000103.png
Normal file
|
After Width: | Height: | Size: 814 KiB |
BIN
android-app/playstore-assets/anon/Screenshot_20260530_000133.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
android-app/playstore-assets/anon/Screenshot_20260530_000230.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
android-app/playstore-assets/anon/Screenshot_20260530_000301.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
android-app/playstore-assets/anon/Screenshot_20260530_000429.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 737 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 977 KiB |
|
After Width: | Height: | Size: 845 KiB |
BIN
android-app/playstore-assets/raw/Screenshot_20260530_000103.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
android-app/playstore-assets/raw/Screenshot_20260530_000301.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 958 KiB |
|
After Width: | Height: | Size: 128 KiB |
@@ -814,7 +814,7 @@
|
|||||||
Einstellungen
|
Einstellungen
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="getAuthStore()?.hasAnyRole('admin', 'vorstand')"
|
v-if="canManageUsers"
|
||||||
to="/cms/benutzer"
|
to="/cms/benutzer"
|
||||||
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"
|
class="block px-4 py-2 text-sm text-yellow-300 hover:text-white hover:bg-primary-700/50 rounded-lg transition-colors"
|
||||||
@click="isMobileMenuOpen = false"
|
@click="isMobileMenuOpen = false"
|
||||||
@@ -849,34 +849,13 @@ const mobileSubmenu = ref(null)
|
|||||||
const mannschaften = ref([])
|
const mannschaften = ref([])
|
||||||
const hasGalleryImages = ref(false)
|
const hasGalleryImages = ref(false)
|
||||||
const showCmsDropdown = ref(false)
|
const showCmsDropdown = ref(false)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
// Lazy store access to avoid Pinia initialization issues
|
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||||
const getAuthStore = () => {
|
const isAdmin = computed(() => authStore.isAdmin)
|
||||||
try {
|
const canAccessNewsletter = computed(() => authStore.hasAnyRole('admin', 'vorstand', 'newsletter'))
|
||||||
return useAuthStore()
|
const canAccessContactRequests = computed(() => authStore.hasAnyRole('admin', 'vorstand', 'trainer'))
|
||||||
} catch (e) {
|
const canManageUsers = computed(() => authStore.hasAnyRole('admin', 'vorstand'))
|
||||||
// Fallback if Pinia is not yet initialized
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reactive auth state from store (lazy)
|
|
||||||
const isLoggedIn = computed(() => {
|
|
||||||
const store = getAuthStore()
|
|
||||||
return store?.isLoggedIn ?? false
|
|
||||||
})
|
|
||||||
const isAdmin = computed(() => {
|
|
||||||
const store = getAuthStore()
|
|
||||||
return store?.isAdmin ?? false
|
|
||||||
})
|
|
||||||
const canAccessNewsletter = computed(() => {
|
|
||||||
const store = getAuthStore()
|
|
||||||
return store?.hasAnyRole('admin', 'vorstand', 'newsletter') ?? false
|
|
||||||
})
|
|
||||||
const canAccessContactRequests = computed(() => {
|
|
||||||
const store = getAuthStore()
|
|
||||||
return store?.hasAnyRole('admin', 'vorstand', 'trainer') ?? false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Automatisches Setzen des Submenus basierend auf der Route
|
// Automatisches Setzen des Submenus basierend auf der Route
|
||||||
const currentSubmenu = computed(() => {
|
const currentSubmenu = computed(() => {
|
||||||
@@ -982,10 +961,7 @@ const handleDocumentClick = (e) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadMannschaften()
|
loadMannschaften()
|
||||||
checkGalleryImages()
|
checkGalleryImages()
|
||||||
const store = getAuthStore()
|
authStore.checkAuth()
|
||||||
if (store) {
|
|
||||||
store.checkAuth()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close CMS dropdown when clicking outside
|
// Close CMS dropdown when clicking outside
|
||||||
document.addEventListener('click', handleDocumentClick)
|
document.addEventListener('click', handleDocumentClick)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"publish-spielplan": "node scripts/publish-imported-spielplan.js",
|
"publish-spielplan": "node scripts/publish-imported-spielplan.js",
|
||||||
"playstore:assets": "./scripts/playstore-assets.sh",
|
"playstore:assets": "./scripts/playstore-assets.sh",
|
||||||
"playstore:anonymize": "./scripts/anonymize-playstore-screenshot.sh",
|
"playstore:anonymize": "./scripts/anonymize-playstore-screenshot.sh",
|
||||||
|
"playstore:screenshots": "./scripts/playstore-screenshot-sizes.sh",
|
||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"lint": "eslint . --fix"
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ IFS=';' read -r -a BOXES <<< "$RECTS"
|
|||||||
for box in "${BOXES[@]}"; do
|
for box in "${BOXES[@]}"; do
|
||||||
IFS=',' read -r x y w h <<< "$box"
|
IFS=',' read -r x y w h <<< "$box"
|
||||||
magick "$TMP" \
|
magick "$TMP" \
|
||||||
\( -size "${w}x${h}" xc:black -alpha set -channel a -evaluate set 70% +channel \) \
|
\( -size "${w}x${h}" xc:black -alpha off \) \
|
||||||
-geometry "+${x}+${y}" -composite "$TMP"
|
-geometry "+${x}+${y}" -composite "$TMP"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
82
scripts/playstore-screenshot-sizes.mjs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { readdir, mkdir } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
|
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
|
||||||
|
function readArg(flag, fallback = '') {
|
||||||
|
const idx = args.indexOf(flag)
|
||||||
|
if (idx === -1) return fallback
|
||||||
|
return args[idx + 1] || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputDirArg = readArg('--input-dir', 'android-app/playstore-assets/anon')
|
||||||
|
const outputDirArg = readArg('--output-dir', 'android-app/playstore-assets/final')
|
||||||
|
const inputDir = path.resolve(rootDir, inputDirArg)
|
||||||
|
const outputDir = path.resolve(rootDir, outputDirArg)
|
||||||
|
|
||||||
|
const profiles = [
|
||||||
|
{ key: 'phone', width: 1080, height: 1920 },
|
||||||
|
{ key: 'tablet-7', width: 1200, height: 1920 },
|
||||||
|
{ key: 'tablet-10', width: 1600, height: 2560 },
|
||||||
|
]
|
||||||
|
|
||||||
|
function isImageFile(name) {
|
||||||
|
const lower = name.toLowerCase()
|
||||||
|
return lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processFile(fileName) {
|
||||||
|
const inputPath = path.join(inputDir, fileName)
|
||||||
|
const parsed = path.parse(fileName)
|
||||||
|
|
||||||
|
for (const profile of profiles) {
|
||||||
|
const profileDir = path.join(outputDir, profile.key)
|
||||||
|
await mkdir(profileDir, { recursive: true })
|
||||||
|
|
||||||
|
const outputPath = path.join(
|
||||||
|
profileDir,
|
||||||
|
`${parsed.name}-${profile.width}x${profile.height}.png`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use contain to preserve all UI content and add solid bars only if needed.
|
||||||
|
await sharp(inputPath)
|
||||||
|
.resize(profile.width, profile.height, {
|
||||||
|
fit: 'contain',
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 1 },
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toFile(outputPath)
|
||||||
|
|
||||||
|
console.log(`Created: ${path.relative(rootDir, outputPath)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const files = await readdir(inputDir)
|
||||||
|
const images = files.filter(isImageFile)
|
||||||
|
|
||||||
|
if (images.length === 0) {
|
||||||
|
console.error(`No PNG/JPG files found in: ${path.relative(rootDir, inputDir)}`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mkdir(outputDir, { recursive: true })
|
||||||
|
for (const image of images) {
|
||||||
|
await processFile(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Done. Output dir: ${path.relative(rootDir, outputDir)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error('Failed to generate screenshot profiles:', error)
|
||||||
|
process.exitCode = 1
|
||||||
|
})
|
||||||
6
scripts/playstore-screenshot-sizes.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
node "$ROOT_DIR/scripts/playstore-screenshot-sizes.mjs" "$@"
|
||||||