feat(app,web,platform): brand all platform icons with the cg leaf monogram
- Master asset assets/icon/app_icon.jpg (2048x2048) plus idempotent
assets/icon/generate_icons.py (Python/Pillow, Lanczos resampling) that
regenerates every platform icon size; preview_contact_sheet.png
documents the rendered variants.
- Web: favicon.png (32px) and icons/Icon-192/512.png gain a feathered
circular alpha mask (removes the master's white JPEG corners); new
Icon-maskable-192/512.png place the logo in the 80% safe circle over
brand sage #D4E4C7; manifest.json and index.html swapped from Flutter
placeholders to "GLP-1 Companion" naming with theme #375A53 /
background #D4E4C7.
- Android: legacy mipmap-*/ic_launcher.png (48-192px) with transparent
corners, plus first-ever adaptive icons: mipmap-anydpi-v26
ic_launcher{,_round}.xml, a sage vector background, and 5-density
foreground PNGs confining the logo to the 66/108dp safe circle.
- iOS: all 15 AppIcon.appiconset PNGs (20px-1024px) rendered opaque RGB
with no alpha channel per App Store validation.
- macOS: app_icon_{16..1024}.png with transparent corners.
- Windows: multi-resolution app_icon.ico (16/24/32/48/64/128/256px).
- Docs: BUILD_STATE.md Post-Phase-7 icon branding entry.
Verified: PIL assertions on dimensions, color mode, alpha, and ICO
frames; adaptive-icon XML well-formedness; flutter build web
--no-tree-shake-icons and flutter build apk --debug package the new
assets.
|
|
@ -82,6 +82,9 @@
|
||||||
- [x] Android toolchain fixes (2026-10-16): `flutter run` on an emulator failed on three pre-existing gaps (Android builds had never been exercised — Phase 6 gates ran on Windows/Web): (1) `flutter_local_notifications` requires core library desugaring → enabled `isCoreLibraryDesugaringEnabled` + `com.android.tools:desugar_jdk_libs:2.1.4` in `android/app/build.gradle.kts`; (2) Kotlin incremental compilation aborts when the pub cache lives on a different drive than the project (C: vs D:, "this and base files have different roots") → `kotlin.incremental=false` in `android/gradle.properties`; (3) the pinned `health` plugin declares minSdk 26 vs. Flutter's default 24 → `minSdk = maxOf(flutter.minSdkVersion, 26)` (Health Connect only ships on Android 8.0+). Verified: `flutter build apk --debug` → `✓ Built build\app\outputs\flutter-apk\app-debug.apk`.
|
- [x] Android toolchain fixes (2026-10-16): `flutter run` on an emulator failed on three pre-existing gaps (Android builds had never been exercised — Phase 6 gates ran on Windows/Web): (1) `flutter_local_notifications` requires core library desugaring → enabled `isCoreLibraryDesugaringEnabled` + `com.android.tools:desugar_jdk_libs:2.1.4` in `android/app/build.gradle.kts`; (2) Kotlin incremental compilation aborts when the pub cache lives on a different drive than the project (C: vs D:, "this and base files have different roots") → `kotlin.incremental=false` in `android/gradle.properties`; (3) the pinned `health` plugin declares minSdk 26 vs. Flutter's default 24 → `minSdk = maxOf(flutter.minSdkVersion, 26)` (Health Connect only ships on Android 8.0+). Verified: `flutter build apk --debug` → `✓ Built build\app\outputs\flutter-apk\app-debug.apk`.
|
||||||
- [x] Mobile overflow fix (2026-10-16): the compound dropdown in `LogInjectionDialog` painted yellow/black stripes on phone widths — `DropdownButtonFormField`'s selected-item Row cannot ellipsize long labels ("Tirzepatide (Mounjaro / Zepbound)") without `isExpanded: true`. Fixed with `isExpanded: true` + `maxLines: 1`/`TextOverflow.ellipsis` on the menu items; regression tests in `test/features/medication/log_injection_dialog_test.dart` pump the dialog at 320 dp (both long labels) and 390 dp asserting no RenderFlex overflow (128/128 tests passing).
|
- [x] Mobile overflow fix (2026-10-16): the compound dropdown in `LogInjectionDialog` painted yellow/black stripes on phone widths — `DropdownButtonFormField`'s selected-item Row cannot ellipsize long labels ("Tirzepatide (Mounjaro / Zepbound)") without `isExpanded: true`. Fixed with `isExpanded: true` + `maxLines: 1`/`TextOverflow.ellipsis` on the menu items; regression tests in `test/features/medication/log_injection_dialog_test.dart` pump the dialog at 320 dp (both long labels) and 390 dp asserting no RenderFlex overflow (128/128 tests passing).
|
||||||
|
|
||||||
|
## Post-Phase-7 Additions
|
||||||
|
- [x] App icon branding (2026-10-16): replaced all stock Flutter icons with the "cg" leaf monogram brand mark. Master asset `assets/icon/app_icon.jpg` (2048×2048); `assets/icon/generate_icons.py` (Python/Pillow, Lanczos) regenerates every platform size idempotently: web (`favicon.png` 32, `icons/Icon-{192,512}.png` with feathered circular alpha, `icons/Icon-maskable-{192,512}.png` at 80% safe-zone over brand sage `#D4E4C7`), Android (legacy `mipmap-*/ic_launcher.png` 48–192 px + new adaptive icons: `mipmap-anydpi-v26/ic_launcher{,_round}.xml`, `drawable/ic_launcher_background.xml` sage vector, `drawable-*/ic_launcher_foreground.png` 66/108 dp safe circle), iOS (all 15 `AppIcon.appiconset` PNGs — opaque RGB, no alpha, per App Store validation), macOS (`app_icon_{16..1024}.png`), Windows (`app_icon.ico` multi-res 16–256 px). Web polish: `manifest.json` name/description/theme (`#375A53` teal, `#D4E4C7` sage) and `index.html` title/description/`theme-color` updated from Flutter placeholders. Verified: PIL dimension/mode/alpha assertions on every output + XML well-formedness + ICO frame enumeration; `flutter build web --no-tree-shake-icons` and `flutter build apk --debug` package the new assets.
|
||||||
|
|
||||||
## Active Verification Gates (Phase 7, 2026-10-16)
|
## Active Verification Gates (Phase 7, 2026-10-16)
|
||||||
- [x] `dart analyze lib test` (Zero issues)
|
- [x] `dart analyze lib test` (Zero issues)
|
||||||
- [x] `flutter test` (all passing — 125/125)
|
- [x] `flutter test` (all passing — 125/125)
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp" android:height="108dp"
|
||||||
|
android:viewportWidth="108" android:viewportHeight="108">
|
||||||
|
<path android:fillColor="#D4E4C7" android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -0,0 +1,147 @@
|
||||||
|
"""Regenerate every platform icon from the master logo.
|
||||||
|
|
||||||
|
Source of truth: assets/icon/app_icon.jpg (2048x2048).
|
||||||
|
Run from anywhere: python assets/icon/generate_icons.py
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
- Web: favicon.png, icons/Icon-{192,512}.png, icons/Icon-maskable-{192,512}.png
|
||||||
|
- Android: mipmap-*/ic_launcher.png (legacy) + adaptive icons (mipmap-anydpi-v26,
|
||||||
|
drawable/ic_launcher_background.xml, drawable-*/ic_launcher_foreground.png)
|
||||||
|
- iOS: every AppIcon.appiconset PNG referenced by Contents.json (opaque, no alpha)
|
||||||
|
- macOS: app_icon_{16..1024}.png (transparent corners, system masks them)
|
||||||
|
- Windows: windows/runner/resources/app_icon.ico (16-256 px, multi-resolution)
|
||||||
|
|
||||||
|
The logo is a full-bleed circle, so standard icons get a feathered circular alpha
|
||||||
|
mask (removes the JPEG's white corners); maskable/adaptive variants scale the
|
||||||
|
circle into the safe zone over the brand sage so any mask crop stays on-brand.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SRC = Path(__file__).parent / "app_icon.jpg"
|
||||||
|
|
||||||
|
# Brand colors sampled from the master logo.
|
||||||
|
SAGE_RGB = (212, 228, 199) # outer ring / maskable background
|
||||||
|
TEAL_RGB = (55, 90, 83) # inner circle (used for web manifest theme)
|
||||||
|
|
||||||
|
RESAMPLE = Image.LANCZOS
|
||||||
|
|
||||||
|
|
||||||
|
def circle_rgba(img: Image.Image) -> Image.Image:
|
||||||
|
"""Return RGBA with a feathered circular alpha mask (design circle spans the full square)."""
|
||||||
|
rgba = img.convert("RGBA")
|
||||||
|
n = rgba.size[0]
|
||||||
|
mask = Image.new("L", (n, n), 0)
|
||||||
|
ImageDraw.Draw(mask).ellipse((0, 0, n - 1, n - 1), fill=255)
|
||||||
|
mask = mask.filter(ImageFilter.GaussianBlur(radius=max(1.0, n / 512)))
|
||||||
|
rgba.putalpha(mask)
|
||||||
|
return rgba
|
||||||
|
|
||||||
|
|
||||||
|
def fit(img: Image.Image, size: int) -> Image.Image:
|
||||||
|
return img.resize((size, size), RESAMPLE)
|
||||||
|
|
||||||
|
|
||||||
|
def save(img: Image.Image, rel: str) -> Path:
|
||||||
|
out = ROOT / rel
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
img.save(out)
|
||||||
|
print(f" {rel} {img.size[0]}x{img.size[1]}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
master = Image.open(SRC)
|
||||||
|
if master.size != (2048, 2048):
|
||||||
|
raise SystemExit(f"master must be 2048x2048, got {master.size}")
|
||||||
|
master_rgb = master.convert("RGB")
|
||||||
|
full_circle = circle_rgba(master_rgb) # transparent corners, brand edge intact
|
||||||
|
|
||||||
|
print("Web:")
|
||||||
|
save(fit(full_circle, 32), "web/favicon.png")
|
||||||
|
save(fit(full_circle, 192), "web/icons/Icon-192.png")
|
||||||
|
save(fit(full_circle, 512), "web/icons/Icon-512.png")
|
||||||
|
for s in (192, 512): # maskable: logo inside the 80% safe circle on brand sage
|
||||||
|
canvas = Image.new("RGB", (s, s), SAGE_RGB)
|
||||||
|
logo = fit(full_circle, int(s * 0.8))
|
||||||
|
canvas.paste(logo, ((s - logo.size[0]) // 2, (s - logo.size[1]) // 2), logo)
|
||||||
|
save(canvas, f"web/icons/Icon-maskable-{s}.png")
|
||||||
|
|
||||||
|
print("Android (legacy densities):")
|
||||||
|
for dpi, px in {"mdpi": 48, "hdpi": 72, "xhdpi": 96, "xxhdpi": 144, "xxxhdpi": 192}.items():
|
||||||
|
save(fit(full_circle, px), f"android/app/src/main/res/mipmap-{dpi}/ic_launcher.png")
|
||||||
|
|
||||||
|
print("Android (adaptive icons):")
|
||||||
|
# Foreground: 108 dp canvas, circular logo confined to the 66 dp safe circle.
|
||||||
|
for dpi, px in {"mdpi": 108, "hdpi": 162, "xhdpi": 216, "xxhdpi": 324, "xxxhdpi": 432}.items():
|
||||||
|
canvas = Image.new("RGBA", (px, px), (0, 0, 0, 0))
|
||||||
|
logo = fit(full_circle, int(px * 66 / 108))
|
||||||
|
canvas.paste(logo, ((px - logo.size[0]) // 2, (px - logo.size[1]) // 2), logo)
|
||||||
|
save(canvas, f"android/app/src/main/res/drawable-{dpi}/ic_launcher_foreground.png")
|
||||||
|
|
||||||
|
bg_xml = ROOT / "android/app/src/main/res/drawable/ic_launcher_background.xml"
|
||||||
|
bg_xml.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
bg_xml.write_text(
|
||||||
|
'<vector xmlns:android="http://schemas.android.com/apk/res/android"\n'
|
||||||
|
' android:width="108dp" android:height="108dp"\n'
|
||||||
|
' android:viewportWidth="108" android:viewportHeight="108">\n'
|
||||||
|
' <path android:fillColor="#%02X%02X%02X" android:pathData="M0,0h108v108h-108z"/>\n'
|
||||||
|
"</vector>\n" % SAGE_RGB,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(" android/app/src/main/res/drawable/ic_launcher_background.xml")
|
||||||
|
|
||||||
|
adaptive_xml = (
|
||||||
|
'<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">\n'
|
||||||
|
' <background android:drawable="@drawable/ic_launcher_background"/>\n'
|
||||||
|
' <foreground android:drawable="@drawable/ic_launcher_foreground"/>\n'
|
||||||
|
"</adaptive-icon>\n"
|
||||||
|
)
|
||||||
|
for name in ("ic_launcher.xml", "ic_launcher_round.xml"):
|
||||||
|
p = ROOT / "android/app/src/main/res/mipmap-anydpi-v26" / name
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(adaptive_xml, encoding="utf-8")
|
||||||
|
print(f" android/app/src/main/res/mipmap-anydpi-v26/{name}")
|
||||||
|
|
||||||
|
print("iOS (opaque - App Store rejects alpha channels):")
|
||||||
|
ios_dir = "ios/Runner/Assets.xcassets/AppIcon.appiconset"
|
||||||
|
ios_sizes = {
|
||||||
|
"Icon-App-20x20@1x.png": 20, "Icon-App-20x20@2x.png": 40, "Icon-App-20x20@3x.png": 60,
|
||||||
|
"Icon-App-29x29@1x.png": 29, "Icon-App-29x29@2x.png": 58, "Icon-App-29x29@3x.png": 87,
|
||||||
|
"Icon-App-40x40@1x.png": 40, "Icon-App-40x40@2x.png": 80, "Icon-App-40x40@3x.png": 120,
|
||||||
|
"Icon-App-60x60@2x.png": 120, "Icon-App-60x60@3x.png": 180,
|
||||||
|
"Icon-App-76x76@1x.png": 76, "Icon-App-76x76@2x.png": 152,
|
||||||
|
"Icon-App-83.5x83.5@2x.png": 167,
|
||||||
|
"Icon-App-1024x1024@1x.png": 1024,
|
||||||
|
}
|
||||||
|
for name, px in ios_sizes.items():
|
||||||
|
fit(master_rgb, px).save(ROOT / ios_dir / name, "PNG")
|
||||||
|
print(f" {ios_dir}/{name} {px}x{px}")
|
||||||
|
|
||||||
|
print("macOS:")
|
||||||
|
mac_dir = "macos/Runner/Assets.xcassets/AppIcon.appiconset"
|
||||||
|
for px in (16, 32, 64, 128, 256, 512, 1024):
|
||||||
|
save(fit(full_circle, px), f"{mac_dir}/app_icon_{px}.png")
|
||||||
|
|
||||||
|
print("Windows:")
|
||||||
|
ico_path = ROOT / "windows/runner/resources/app_icon.ico"
|
||||||
|
fit(full_circle, 256).save(
|
||||||
|
ico_path,
|
||||||
|
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||||
|
)
|
||||||
|
print(" windows/runner/resources/app_icon.ico frames: 16-256 px")
|
||||||
|
|
||||||
|
# Sanity checks on the 512 web icon and the ICO.
|
||||||
|
chk = Image.open(ROOT / "web/icons/Icon-512.png").convert("RGBA")
|
||||||
|
c = chk.size[0] // 2
|
||||||
|
r, g, b, a = chk.getpixel((c, c))
|
||||||
|
assert g > r and a == 255, f"center should be opaque teal, got {(r, g, b, a)}"
|
||||||
|
assert chk.getpixel((2, 2))[3] == 0, "corner should be transparent"
|
||||||
|
assert Image.open(ico_path).format == "ICO"
|
||||||
|
print("OK: all icons generated and sanity-checked.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 921 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1010 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 943 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 887 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.8 KiB |
BIN
web/favicon.png
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 127 KiB |
|
|
@ -18,18 +18,19 @@
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="Cross-platform GLP-1 medication, nutrition, and pantry tracking companion.">
|
||||||
|
<meta name="theme-color" content="#375A53">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="glpcompanion">
|
<meta name="apple-mobile-web-app-title" content="GLP Companion">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>glpcompanion</title>
|
<title>GLP-1 Companion</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "glpcompanion",
|
"name": "GLP-1 Companion",
|
||||||
"short_name": "glpcompanion",
|
"short_name": "GLP Companion",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#D4E4C7",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#375A53",
|
||||||
"description": "A new Flutter project.",
|
"description": "Cross-platform GLP-1 medication, nutrition, and pantry tracking companion.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 95 KiB |