From 94fc19f6f21cdfe76a09e3f3e3520e5924237844 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Sun, 1 Feb 2026 16:51:13 +0100 Subject: [PATCH] Build: add icon generation to CI and scripts, bump version to 4.16.0 - Run app icon and launcher icon generation in build-mobile workflow - Add generate-mobile-icon scripts (Python/Pillow, ImageMagick, Inkscape) - BUILD.md: document icon requirements and troubleshooting - setup.py: version 4.16.0 Co-authored-by: Cursor --- .github/workflows/build-mobile.yml | 18 +++++++ BUILD.md | 4 ++ scripts/build-all.bat | 12 +++++ scripts/build-all.sh | 10 ++++ scripts/build-mobile.bat | 12 +++++ scripts/build-mobile.sh | 12 +++++ scripts/generate-mobile-icon.bat | 23 +++++++++ scripts/generate-mobile-icon.py | 75 ++++++++++++++++++++++++++++++ scripts/generate-mobile-icon.sh | 28 +++++++++++ setup.py | 2 +- 10 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-mobile-icon.bat create mode 100644 scripts/generate-mobile-icon.py create mode 100644 scripts/generate-mobile-icon.sh diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 4029b5c5..817f2bde 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -37,6 +37,14 @@ jobs: working-directory: mobile run: flutter pub get + - name: Generate app icons + run: | + pip install Pillow + python scripts/generate-mobile-icon.py + - name: Generate launcher icons + working-directory: mobile + run: dart run flutter_launcher_icons + # Skip tests for now - mobile app is incomplete (tests exist but lib/ source code is missing) # - name: Run tests # working-directory: mobile @@ -74,6 +82,16 @@ jobs: working-directory: mobile run: flutter create --platforms=ios . + - name: Generate app icons + run: | + pip install Pillow + python scripts/generate-mobile-icon.py + - name: Generate launcher icons + working-directory: mobile + run: | + dart run flutter_launcher_icons + dart run flutter_launcher_icons -f flutter_launcher_icons_ios.yaml + - name: Configure iOS project for device build without code signing working-directory: mobile run: | diff --git a/BUILD.md b/BUILD.md index c9997f29..f5533f0b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -87,6 +87,7 @@ scripts\build-all.bat --windows-only - Flutter SDK 3.0+ - Android SDK (for Android) - Xcode (for iOS, macOS only) +- **App icon:** Launcher icons are generated at the start of each mobile build from `mobile/assets/icon/app_icon.png`. That PNG can be exported once from `app/static/images/timetracker-logo-icon.svg` (1024×1024), or created by running `scripts/generate-mobile-icon.bat` / `scripts/generate-mobile-icon.sh` (requires ImageMagick, Inkscape, or Python with Pillow). ### Desktop App - Node.js 18+ @@ -107,6 +108,9 @@ scripts\build-all.bat --windows-only ## Troubleshooting +- **Mobile launcher icon shows Android default:** Run icon generation and do a full clean build: from `mobile/` run `flutter clean`, `flutter pub get`, `dart run flutter_launcher_icons`, then build again. The build scripts run icon generation automatically; if you built without them, run the above once. +- **Icon should match the web app:** Export `app/static/images/timetracker-logo-icon.svg` to 1024×1024 PNG at `mobile/assets/icon/app_icon.png` (see `mobile/assets/icon/README.md`), then run `dart run flutter_launcher_icons` and rebuild. + See `scripts/README-BUILD.md` for detailed troubleshooting guide. ## CI/CD diff --git a/scripts/build-all.bat b/scripts/build-all.bat index 6e622bd0..ffd5e6ae 100644 --- a/scripts/build-all.bat +++ b/scripts/build-all.bat @@ -123,6 +123,18 @@ if "%BUILD_MOBILE%"=="1" ( echo [OK] Dependencies installed echo. + echo [1b/6] Generating app icons... + cd /d "%PROJECT_ROOT%" + call "%SCRIPT_DIR%generate-mobile-icon.bat" + cd /d "%PROJECT_ROOT%\mobile" + call dart run flutter_launcher_icons + if errorlevel 1 ( + echo [ERROR] Failed to generate launcher icons + exit /b 1 + ) + echo [OK] Launcher icons generated + echo. + echo [2/6] Analyzing Flutter code... call flutter analyze if errorlevel 1 ( diff --git a/scripts/build-all.sh b/scripts/build-all.sh index d8255595..05e88403 100644 --- a/scripts/build-all.sh +++ b/scripts/build-all.sh @@ -84,6 +84,16 @@ build_mobile() { # Get dependencies print_header "Installing Flutter dependencies" flutter pub get + + # Generate app icons (source PNG then launcher sizes) + cd "$PROJECT_ROOT" + "$SCRIPT_DIR/generate-mobile-icon.sh" || true + cd "$PROJECT_ROOT/mobile" + dart run flutter_launcher_icons + if [ $? -ne 0 ]; then + print_error "Failed to generate launcher icons" + exit 1 + fi # Analyze code print_header "Analyzing Flutter code" diff --git a/scripts/build-mobile.bat b/scripts/build-mobile.bat index 5f4e97c9..3f4baa19 100644 --- a/scripts/build-mobile.bat +++ b/scripts/build-mobile.bat @@ -41,6 +41,18 @@ if errorlevel 1 ( ) echo. +REM Generate app icons (source PNG then launcher sizes) +cd /d "%PROJECT_ROOT%" +call "%SCRIPT_DIR%generate-mobile-icon.bat" +cd /d "%MOBILE_DIR%" +echo Generating launcher icons... +call dart run flutter_launcher_icons +if errorlevel 1 ( + echo ERROR: Failed to generate launcher icons + exit /b 1 +) +echo. + REM Analyze echo Analyzing code... call flutter analyze diff --git a/scripts/build-mobile.sh b/scripts/build-mobile.sh index be6a7cf7..228a8781 100644 --- a/scripts/build-mobile.sh +++ b/scripts/build-mobile.sh @@ -38,6 +38,18 @@ echo "Installing dependencies..." flutter pub get echo "" +# Generate app icons (source PNG then launcher sizes) +cd "$PROJECT_ROOT" +"$SCRIPT_DIR/generate-mobile-icon.sh" || true +cd "$MOBILE_DIR" +echo "Generating launcher icons..." +dart run flutter_launcher_icons +if [ $? -ne 0 ]; then + echo "ERROR: Failed to generate launcher icons" + exit 1 +fi +echo "" + # Analyze echo "Analyzing code..." flutter analyze || true diff --git a/scripts/generate-mobile-icon.bat b/scripts/generate-mobile-icon.bat new file mode 100644 index 00000000..ae5813d2 --- /dev/null +++ b/scripts/generate-mobile-icon.bat @@ -0,0 +1,23 @@ +@echo off +REM Generate mobile app icon (app_icon.png) from SVG or Python fallback. +setlocal +set SCRIPT_DIR=%~dp0 +set PROJECT_ROOT=%SCRIPT_DIR%.. +set SVG=%PROJECT_ROOT%\app\static\images\timetracker-logo-icon.svg +set OUT=%PROJECT_ROOT%\mobile\assets\icon\app_icon.png + +if not exist "%PROJECT_ROOT%\mobile\assets\icon" mkdir "%PROJECT_ROOT%\mobile\assets\icon" + +REM Try ImageMagick first (exact SVG export) +where magick >nul 2>&1 +if %errorlevel% equ 0 ( + magick "%SVG%" -resize 1024x1024 "%OUT%" + if %errorlevel% equ 0 ( + echo Generated app_icon.png with ImageMagick + exit /b 0 + ) +) + +REM Fallback: Python script (requires Pillow) +python "%SCRIPT_DIR%generate-mobile-icon.py" +exit /b %errorlevel% diff --git a/scripts/generate-mobile-icon.py b/scripts/generate-mobile-icon.py new file mode 100644 index 00000000..dd9d6229 --- /dev/null +++ b/scripts/generate-mobile-icon.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Generate mobile app icon (app_icon.png) matching the web app's timetracker-logo-icon.svg. +Creates a 1024x1024 PNG: gradient rounded rect, white clock circle, hour marks, checkmark. +Requires: pip install Pillow +""" +import os +import sys + +def main(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(script_dir) + out_dir = os.path.join(project_root, "mobile", "assets", "icon") + out_path = os.path.join(out_dir, "app_icon.png") + + try: + from PIL import Image, ImageDraw + except ImportError: + print("Pillow not installed. Run: pip install Pillow") + print("Or use ImageMagick: magick app/static/images/timetracker-logo-icon.svg -resize 1024x1024 mobile/assets/icon/app_icon.png") + return 1 + + os.makedirs(out_dir, exist_ok=True) + + # Match SVG: 512 viewBox scaled to 1024 (scale 2) + size = 1024 + r_rect = 256 # 128 * 2 + cx, cy = size // 2, size // 2 + r_clock = 360 # 180 * 2 + stroke_circle = 64 # 32 * 2 + stroke_mark = 48 # 24 * 2 + stroke_check = 80 # 40 * 2 + + # 1) Gradient image (linear top-left #4A90E2 to bottom-right #50E3C2) + grad = Image.new("RGB", (size, size), (0, 0, 0)) + px = grad.load() + for y in range(size): + for x in range(size): + t = (x + y) / (2 * size) + t = max(0, min(1, t)) + r = int(0x4A + (0x50 - 0x4A) * t) + g = int(0x90 + (0xE3 - 0x90) * t) + b = int(0xE2 + (0xC2 - 0xE2) * t) + px[x, y] = (r, g, b) + + # 2) Rounded rect mask + mask = Image.new("L", (size, size), 0) + ImageDraw.Draw(mask).rounded_rectangle([0, 0, size - 1, size - 1], radius=r_rect, fill=255) + + # 3) Base image: gradient only inside rounded rect + base = Image.new("RGB", (size, size), (0x4A, 0x90, 0xE2)) + base.paste(grad, (0, 0), mask) + draw = ImageDraw.Draw(base) + + # 4) White circle (stroke only): outer circle white, inner circle = mid gradient + draw.ellipse([cx - r_clock, cy - r_clock, cx + r_clock, cy + r_clock], fill="white", outline=None) + inner_r = r_clock - stroke_circle + mid = ((0x4A + 0x50) // 2, (0x90 + 0xE3) // 2, (0xE2 + 0xC2) // 2) + draw.ellipse([cx - inner_r, cy - inner_r, cx + inner_r, cy + inner_r], fill=mid, outline=None) + + # 5) Hour marks (4 white lines) - SVG positions scaled *2 + draw.line([(cx, cy - r_clock), (cx, cy - r_clock + stroke_mark)], fill="white", width=stroke_mark) + draw.line([(cx, cy + r_clock - stroke_mark), (cx, cy + r_clock)], fill="white", width=stroke_mark) + draw.line([(cx - r_clock, cy), (cx - r_clock + stroke_mark, cy)], fill="white", width=stroke_mark) + draw.line([(cx + r_clock - stroke_mark, cy), (cx + r_clock, cy)], fill="white", width=stroke_mark) + + # 6) Checkmark: M 195 270 L 255 330 L 365 220, stroke 40, round. Scale *2 + draw.line([(390, 540), (510, 660), (730, 440)], fill="white", width=stroke_check, joint="curve") + + base.save(out_path) + print(f"Created {out_path}") + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/generate-mobile-icon.sh b/scripts/generate-mobile-icon.sh new file mode 100644 index 00000000..f98d35f7 --- /dev/null +++ b/scripts/generate-mobile-icon.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Generate mobile app icon (app_icon.png) from SVG or Python fallback. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SVG="$PROJECT_ROOT/app/static/images/timetracker-logo-icon.svg" +OUT="$PROJECT_ROOT/mobile/assets/icon/app_icon.png" + +mkdir -p "$(dirname "$OUT")" + +# Try ImageMagick first (exact SVG export) +if command -v magick &> /dev/null; then + if magick "$SVG" -resize 1024x1024 "$OUT"; then + echo "Generated app_icon.png with ImageMagick" + exit 0 + fi +fi + +# Try Inkscape +if command -v inkscape &> /dev/null; then + if inkscape "$SVG" -w 1024 -h 1024 -o "$OUT"; then + echo "Generated app_icon.png with Inkscape" + exit 0 + fi +fi + +# Fallback: Python script (requires Pillow) +python3 "$SCRIPT_DIR/generate-mobile-icon.py" +exit $? diff --git a/setup.py b/setup.py index c4f25834..d8874e8f 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages setup( name='timetracker', - version='4.15.1', + version='4.16.0', packages=find_packages(), include_package_data=True, install_requires=[