fix(app): the app has its own icon instead of Flutter's
All checks were successful
CI / check (push) Successful in 5s
CI / deploy (push) Successful in 1m3s

The launcher showed Flutter's logo: the project template's icons were
never replaced. tool/app_icons.py now makes every icon from the
kingfisher (assets/icon/halkyon_icon.png), through flutter_launcher_icons
v0.14.4 configured in the app's pubspec: Android's adaptive icon (the
kingfisher inset 16 % on white, for each launcher to cut to its shape)
and the square one before Android 8; iOS's, without transparency;
macOS's, the rounded tile on Apple's grid (824 of 1024 pixels); and
Windows', the rounded tile, its .ico with every size from 16 to 256
pixels (the tool writes only 256). It keeps the iOS project file as it
was, which the tool would break (flutter_launcher_icons issue 634), and
writes the snap's icon as tool/snap_icon.py did, which it replaces.

The Linux runner gives its window the bundled icon too, for desktops
that don't take it from the desktop file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-06 22:49:25 +02:00
commit e291d3af7a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 251 additions and 238 deletions

84
tool/app_icons.py Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Makes the app's icons for every platform from its own, the kingfisher on white
(halkyon_learn_flutter/assets/icon/halkyon_icon.png).
- The snap's, snap/gui/halkyon-learn.png (512 x 512 pixels, the Snap Store's size),
which the Store and the desktop file show as they are: the picture with rounded
corners, as other apps' white tiles have (radius 142.222 of 512, about 28 %).
- Through flutter_launcher_icons, configured in the app's pubspec: Android's adaptive
icon and its older square one, iOS's, macOS's (the rounded tile on Apple's grid,
824 of 1024 pixels) and Windows' (the rounded tile).
- Windows' .ico once more, with every size Windows shows (16 to 256 pixels): the tool
writes only the 256 one and leaves Windows to scale it down for its small views.
- The iOS project file as it was: the tool sets every build setting with ASSETCATALOG
in its name to the icon set's name, Xcode's yes-or-no
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS too, which breaks the
project (flutter_launcher_icons issue 634); the project names its icon set already.
The derived images it hands to the tool go to halkyon_learn_flutter/build/icons/,
which git ignores. The rounded corners' mask is drawn at four times the size and
scaled down, so their edges are smooth.
Run from the repository root with Python 3 and Pillow (python3-pil on Ubuntu), the
app's dependencies fetched:
python3 tool/app_icons.py
"""
import subprocess
from pathlib import Path
from PIL import Image, ImageDraw
APP = Path("halkyon_learn_flutter")
SOURCE = APP / "assets/icon/halkyon_icon.png"
DERIVED = APP / "build/icons"
SNAP_ICON = Path("snap/gui/halkyon-learn.png")
WINDOWS_ICON = APP / "windows/runner/resources/app_icon.ico"
IOS_PROJECT = APP / "ios/Runner.xcodeproj/project.pbxproj"
RADIUS = 142.222 / 512
SUPERSAMPLING = 4
# Apple's grid: the tile is 824 of the icon's 1024 pixels, centred.
MACOS_CANVAS = 1024
MACOS_TILE = 824
WINDOWS_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256]
def rounded(size: int) -> Image.Image:
"""The icon at [size] pixels, its corners rounded off to transparency."""
icon = Image.open(SOURCE).convert("RGBA").resize((size, size), Image.LANCZOS)
large = size * SUPERSAMPLING
mask = Image.new("L", (large, large), 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, large - 1, large - 1), radius=RADIUS * large, fill=255
)
mask = mask.resize((size, size), Image.LANCZOS)
# Keeps what the icon itself makes transparent, and takes the corners away.
transparent = Image.new("L", (size, size), 0)
icon.putalpha(Image.composite(icon.getchannel("A"), transparent, mask))
return icon
def main() -> None:
rounded(512).save(SNAP_ICON, optimize=True)
print(f"Wrote {SNAP_ICON}.")
DERIVED.mkdir(parents=True, exist_ok=True)
rounded(1024).save(DERIVED / "rounded.png")
macos = Image.new("RGBA", (MACOS_CANVAS, MACOS_CANVAS), (0, 0, 0, 0))
margin = (MACOS_CANVAS - MACOS_TILE) // 2
macos.alpha_composite(rounded(MACOS_TILE), (margin, margin))
macos.save(DERIVED / "macos.png")
ios_project = IOS_PROJECT.read_bytes()
subprocess.run(["dart", "run", "flutter_launcher_icons"], cwd=APP, check=True)
IOS_PROJECT.write_bytes(ios_project)
rounded(256).save(
WINDOWS_ICON, format="ICO", sizes=[(size, size) for size in WINDOWS_SIZES]
)
print(f"Wrote {WINDOWS_ICON} with {len(WINDOWS_SIZES)} sizes.")
if __name__ == "__main__":
main()

View file

@ -1,47 +0,0 @@
#!/usr/bin/env python3
"""Makes the snap's icon from the app's: the same picture with rounded corners.
The app's icon is a white square, which the Snap Store and the desktop's dock
show as it is, so the snap's icon rounds its corners off to transparency. The
radius, 142.222 of 512 pixels (about 28 %), is the one other desktop apps'
white rounded tiles use. The mask is drawn at four times the size and scaled
down, so the corners' edges are smooth.
Run from the repository root with Python 3 and Pillow (python3-pil on Ubuntu):
python3 tool/snap_icon.py
It rewrites snap/gui/halkyon-learn.png (512 x 512 pixels, the Store's size),
used by both the Store listing and the desktop file.
"""
from pathlib import Path
from PIL import Image, ImageDraw
SOURCE = Path("halkyon_learn_flutter/assets/icon/halkyon_icon.png")
TARGET = Path("snap/gui/halkyon-learn.png")
SIZE = 512
RADIUS = 142.222
SUPERSAMPLING = 4
def main() -> None:
icon = Image.open(SOURCE).convert("RGBA").resize((SIZE, SIZE), Image.LANCZOS)
large = SIZE * SUPERSAMPLING
mask = Image.new("L", (large, large), 0)
ImageDraw.Draw(mask).rounded_rectangle(
(0, 0, large - 1, large - 1), radius=RADIUS * SUPERSAMPLING, fill=255
)
mask = mask.resize((SIZE, SIZE), Image.LANCZOS)
# Keeps what the icon itself makes transparent, and takes the corners away.
transparent = Image.new("L", (SIZE, SIZE), 0)
icon.putalpha(Image.composite(icon.getchannel("A"), transparent, mask))
icon.save(TARGET, optimize=True)
print(f"Wrote {TARGET} ({TARGET.stat().st_size} bytes).")
if __name__ == "__main__":
main()