Icona dell'app: squircle verde con nota musicale
Creata un'icona coerente con la palette dell'app: squircle Apple-style (corner radius ~22%), gradient verde Spotify -> verde scuro, nota musicale (♫) bianca centrata con leggero glow, sottile highlight in alto e bordo soft per definizione. - assets/generate_icon.py: script PIL riproducibile, produce icon.png (master 1024) -> icon.iconset/ -> iconutil -> icon.icns (macOS) e icon.ico multi-size (Windows) - assets/icon.png / icon.icns / icon.ico committati - assets/icon.iconset/ in .gitignore (intermedio rigenerabile) - build_macos.py: --icon assets/icon.icns se presente - build_windows.py: --icon assets/icon.ico se presente Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
878e024c13
commit
52002f5184
7 files changed
+249
-4
No files matched your search
@@ -40,3 +40,6 @@ Vibes_List.txt
|
|||||||
Beatport Top 100*.txt
|
Beatport Top 100*.txt
|
||||||
PlayList_2026.txt
|
PlayList_2026.txt
|
||||||
PlayList_da_scaricare.txt
|
PlayList_da_scaricare.txt
|
||||||
|
|
||||||
|
# Asset intermedi rigenerabili (vedi assets/generate_icon.py)
|
||||||
|
assets/icon.iconset/
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
"""Genera l'icona dell'app (PNG/ICNS/ICO) in modo riproducibile.
|
||||||
|
|
||||||
|
Stile: squircle (Apple-style rounded rect) con gradient verde Spotify
|
||||||
|
verso nero, nota musicale bianca al centro con leggero glow, highlight
|
||||||
|
sottile in alto.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
python3 assets/generate_icon.py
|
||||||
|
|
||||||
|
Produce:
|
||||||
|
assets/icon.png (1024x1024, master)
|
||||||
|
assets/icon.icns (macOS, generato via iconutil)
|
||||||
|
assets/icon.ico (Windows, multi-size)
|
||||||
|
assets/icon.iconset/... (intermedio macOS)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||||
|
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
ROOT = HERE.parent
|
||||||
|
|
||||||
|
SIZE = 1024
|
||||||
|
RADIUS_RATIO = 0.225 # ~22% del lato, Apple iOS-like squircle
|
||||||
|
PADDING_RATIO = 0.085 # margine fra bordo immagine e squircle
|
||||||
|
|
||||||
|
# Palette (coerente col tema della UI)
|
||||||
|
GREEN_TOP = (30, 215, 96) # verde Spotify lucido
|
||||||
|
GREEN_BOTTOM = (15, 59, 34) # verde scuro profondo
|
||||||
|
WHITE = (255, 255, 255, 255)
|
||||||
|
HIGHLIGHT = (255, 255, 255, 38) # bianco semitrasparente per il riflesso
|
||||||
|
|
||||||
|
|
||||||
|
def vertical_gradient(size: int, top: Tuple[int, int, int],
|
||||||
|
bottom: Tuple[int, int, int]) -> Image.Image:
|
||||||
|
"""Crea un gradient verticale top -> bottom."""
|
||||||
|
img = Image.new("RGB", (size, size), top)
|
||||||
|
px = img.load()
|
||||||
|
for y in range(size):
|
||||||
|
t = y / (size - 1)
|
||||||
|
# ease (quad) per ammorbidire la transizione
|
||||||
|
t = t * t
|
||||||
|
r = int(top[0] + (bottom[0] - top[0]) * t)
|
||||||
|
g = int(top[1] + (bottom[1] - top[1]) * t)
|
||||||
|
b = int(top[2] + (bottom[2] - top[2]) * t)
|
||||||
|
for x in range(size):
|
||||||
|
px[x, y] = (r, g, b)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def squircle_mask(size: int, radius: int, padding: int) -> Image.Image:
|
||||||
|
"""Maschera alpha per un rounded rect centrato con padding."""
|
||||||
|
mask = Image.new("L", (size, size), 0)
|
||||||
|
draw = ImageDraw.Draw(mask)
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
(padding, padding, size - padding, size - padding),
|
||||||
|
radius=radius, fill=255,
|
||||||
|
)
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def find_music_font() -> str:
|
||||||
|
"""Trova un font che contenga il glifo della nota musicale (♫)."""
|
||||||
|
candidates = [
|
||||||
|
"/System/Library/Fonts/Apple Symbols.ttf",
|
||||||
|
"/System/Library/Fonts/Symbol.ttf",
|
||||||
|
"/System/Library/Fonts/SFNS.ttf",
|
||||||
|
"/Library/Fonts/Arial Unicode.ttf",
|
||||||
|
]
|
||||||
|
for f in candidates:
|
||||||
|
if Path(f).exists():
|
||||||
|
return f
|
||||||
|
raise RuntimeError("Nessun font Unicode con simboli trovato")
|
||||||
|
|
||||||
|
|
||||||
|
def draw_note(img: Image.Image, glyph: str = "♫") -> None:
|
||||||
|
"""Stampa la nota musicale centrata, in bianco con leggero glow."""
|
||||||
|
font_path = find_music_font()
|
||||||
|
target_pt = int(SIZE * 0.62)
|
||||||
|
font = ImageFont.truetype(font_path, target_pt)
|
||||||
|
|
||||||
|
cx, cy = SIZE // 2, SIZE // 2 - int(SIZE * 0.01)
|
||||||
|
|
||||||
|
# Glow: stessa nota disegnata sfocata sotto in bianco semitrasparente
|
||||||
|
glow_layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||||
|
glow_draw = ImageDraw.Draw(glow_layer)
|
||||||
|
glow_draw.text((cx, cy), glyph, font=font, fill=(255, 255, 255, 110),
|
||||||
|
anchor="mm")
|
||||||
|
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=24))
|
||||||
|
img.alpha_composite(glow_layer)
|
||||||
|
|
||||||
|
# Testo nitido sopra
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
draw.text((cx, cy), glyph, font=font, fill=WHITE, anchor="mm")
|
||||||
|
|
||||||
|
|
||||||
|
def make_master() -> Image.Image:
|
||||||
|
"""Genera l'immagine master 1024x1024 RGBA con tutto."""
|
||||||
|
padding = int(SIZE * PADDING_RATIO)
|
||||||
|
radius = int(SIZE * RADIUS_RATIO)
|
||||||
|
|
||||||
|
# Background gradient verde
|
||||||
|
gradient = vertical_gradient(SIZE, GREEN_TOP, GREEN_BOTTOM)
|
||||||
|
|
||||||
|
# Squircle: prendiamo il gradient mascherato come forma
|
||||||
|
mask = squircle_mask(SIZE, radius, padding)
|
||||||
|
base = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||||
|
base.paste(gradient, (0, 0), mask)
|
||||||
|
|
||||||
|
# Highlight in alto: banda sottile (tipo "shine" iOS), molto trasparente
|
||||||
|
hl_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||||
|
hl_draw = ImageDraw.Draw(hl_layer)
|
||||||
|
hl_top = padding + int(SIZE * 0.04)
|
||||||
|
hl_bot = padding + int(SIZE * 0.16)
|
||||||
|
hl_left = padding + int(SIZE * 0.10)
|
||||||
|
hl_right = SIZE - padding - int(SIZE * 0.10)
|
||||||
|
hl_draw.ellipse(
|
||||||
|
(hl_left, hl_top, hl_right, hl_bot),
|
||||||
|
fill=(255, 255, 255, 30),
|
||||||
|
)
|
||||||
|
hl_layer = hl_layer.filter(ImageFilter.GaussianBlur(radius=18))
|
||||||
|
masked = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||||
|
masked.paste(hl_layer, (0, 0), mask)
|
||||||
|
base.alpha_composite(masked)
|
||||||
|
|
||||||
|
# Bordo interno sottile per dare definizione (stroke da 0.5%)
|
||||||
|
ring = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||||
|
ring_draw = ImageDraw.Draw(ring)
|
||||||
|
ring_draw.rounded_rectangle(
|
||||||
|
(padding, padding, SIZE - padding, SIZE - padding),
|
||||||
|
radius=radius,
|
||||||
|
outline=(255, 255, 255, 28),
|
||||||
|
width=max(2, int(SIZE * 0.004)),
|
||||||
|
)
|
||||||
|
base.alpha_composite(ring)
|
||||||
|
|
||||||
|
# Nota musicale al centro
|
||||||
|
draw_note(base)
|
||||||
|
|
||||||
|
# Lieve ombra interna in basso per profondita (vignette)
|
||||||
|
shade = Image.new("L", (SIZE, SIZE), 0)
|
||||||
|
sd = ImageDraw.Draw(shade)
|
||||||
|
sd.rounded_rectangle(
|
||||||
|
(padding, padding, SIZE - padding, SIZE - padding),
|
||||||
|
radius=radius, outline=255, width=int(SIZE * 0.025),
|
||||||
|
)
|
||||||
|
shade = shade.filter(ImageFilter.GaussianBlur(radius=int(SIZE * 0.025)))
|
||||||
|
overlay = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||||
|
overlay.paste((0, 0, 0, 60), mask=shade)
|
||||||
|
overlay = Image.new("RGBA", overlay.size, (0, 0, 0, 0)).convert("RGBA")
|
||||||
|
# Skip vignette complicata — base e' gia' coerente
|
||||||
|
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def export_iconset(master: Image.Image, outdir: Path) -> None:
|
||||||
|
"""Esporta le dimensioni standard macOS in outdir."""
|
||||||
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
sizes = [
|
||||||
|
(16, "icon_16x16.png"),
|
||||||
|
(32, "icon_16x16@2x.png"),
|
||||||
|
(32, "icon_32x32.png"),
|
||||||
|
(64, "icon_32x32@2x.png"),
|
||||||
|
(128, "icon_128x128.png"),
|
||||||
|
(256, "icon_128x128@2x.png"),
|
||||||
|
(256, "icon_256x256.png"),
|
||||||
|
(512, "icon_256x256@2x.png"),
|
||||||
|
(512, "icon_512x512.png"),
|
||||||
|
(1024, "icon_512x512@2x.png"),
|
||||||
|
]
|
||||||
|
for px, name in sizes:
|
||||||
|
img = master.resize((px, px), Image.LANCZOS)
|
||||||
|
img.save(outdir / name, "PNG")
|
||||||
|
|
||||||
|
|
||||||
|
def build_icns(iconset_dir: Path, output_icns: Path) -> bool:
|
||||||
|
"""Usa iconutil di macOS per generare il .icns. Ritorna True se ok."""
|
||||||
|
if not shutil.which("iconutil"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["iconutil", "-c", "icns", str(iconset_dir), "-o", str(output_icns)],
|
||||||
|
check=True, capture_output=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def build_ico(master: Image.Image, output_ico: Path) -> None:
|
||||||
|
"""Genera il .ico multi-size per Windows."""
|
||||||
|
sizes = [(16, 16), (24, 24), (32, 32), (48, 48),
|
||||||
|
(64, 64), (128, 128), (256, 256)]
|
||||||
|
master.save(output_ico, format="ICO",
|
||||||
|
sizes=sizes, append_images=[])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f">>> Genero icona master {SIZE}x{SIZE}...")
|
||||||
|
master = make_master()
|
||||||
|
master_path = HERE / "icon.png"
|
||||||
|
master.save(master_path, "PNG")
|
||||||
|
print(f" {master_path}")
|
||||||
|
|
||||||
|
iconset_dir = HERE / "icon.iconset"
|
||||||
|
if iconset_dir.exists():
|
||||||
|
shutil.rmtree(iconset_dir)
|
||||||
|
print(">>> Esporto iconset macOS...")
|
||||||
|
export_iconset(master, iconset_dir)
|
||||||
|
|
||||||
|
icns_path = HERE / "icon.icns"
|
||||||
|
if build_icns(iconset_dir, icns_path):
|
||||||
|
print(f" {icns_path}")
|
||||||
|
else:
|
||||||
|
print(" SKIP icns (iconutil non disponibile)")
|
||||||
|
|
||||||
|
print(">>> Genero icon.ico per Windows...")
|
||||||
|
ico_path = HERE / "icon.ico"
|
||||||
|
build_ico(master, ico_path)
|
||||||
|
print(f" {ico_path}")
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
+6
-1
@@ -208,12 +208,17 @@ def run_pyinstaller():
|
|||||||
if f.is_file():
|
if f.is_file():
|
||||||
add_binaries.extend(["--add-binary", f"{f}:lib"])
|
add_binaries.extend(["--add-binary", f"{f}:lib"])
|
||||||
|
|
||||||
|
icon_path = ROOT / "assets" / "icon.icns"
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable, "-m", "PyInstaller",
|
sys.executable, "-m", "PyInstaller",
|
||||||
"--name", "MusicTools",
|
"--name", "MusicTools",
|
||||||
"--windowed",
|
"--windowed",
|
||||||
"--onedir",
|
"--onedir",
|
||||||
"--noconfirm",
|
"--noconfirm",
|
||||||
|
]
|
||||||
|
if icon_path.exists():
|
||||||
|
cmd.extend(["--icon", str(icon_path)])
|
||||||
|
cmd.extend([
|
||||||
# Web UI assets (HTML/CSS/JS) — usati da pywebview
|
# Web UI assets (HTML/CSS/JS) — usati da pywebview
|
||||||
"--add-data", f"{ROOT / 'webui'}:webui",
|
"--add-data", f"{ROOT / 'webui'}:webui",
|
||||||
# Hidden imports per pywebview (WebKit su macOS)
|
# Hidden imports per pywebview (WebKit su macOS)
|
||||||
@@ -225,7 +230,7 @@ def run_pyinstaller():
|
|||||||
*add_binaries,
|
*add_binaries,
|
||||||
# Entry point
|
# Entry point
|
||||||
str(ROOT / "main.py"),
|
str(ROOT / "main.py"),
|
||||||
]
|
])
|
||||||
|
|
||||||
log(f"Comando: {' '.join(cmd[:10])}...")
|
log(f"Comando: {' '.join(cmd[:10])}...")
|
||||||
subprocess.run(cmd, check=True, cwd=str(ROOT))
|
subprocess.run(cmd, check=True, cwd=str(ROOT))
|
||||||
|
|||||||
+6
-3
@@ -99,14 +99,17 @@ def run_pyinstaller():
|
|||||||
if f.is_file() and f.suffix in (".exe", ""):
|
if f.is_file() and f.suffix in (".exe", ""):
|
||||||
add_binaries.extend(["--add-binary", f"{f};."])
|
add_binaries.extend(["--add-binary", f"{f};."])
|
||||||
|
|
||||||
|
icon_path = ROOT / "assets" / "icon.ico"
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable, "-m", "PyInstaller",
|
sys.executable, "-m", "PyInstaller",
|
||||||
"--name", "MusicTools",
|
"--name", "MusicTools",
|
||||||
"--windowed",
|
"--windowed",
|
||||||
"--onedir",
|
"--onedir",
|
||||||
"--noconfirm",
|
"--noconfirm",
|
||||||
# Icona (se presente)
|
]
|
||||||
# "--icon", str(ROOT / "icon.ico"),
|
if icon_path.exists():
|
||||||
|
cmd.extend(["--icon", str(icon_path)])
|
||||||
|
cmd.extend([
|
||||||
# Web UI assets (HTML/CSS/JS) — usati da pywebview
|
# Web UI assets (HTML/CSS/JS) — usati da pywebview
|
||||||
"--add-data", f"{ROOT / 'webui'};webui",
|
"--add-data", f"{ROOT / 'webui'};webui",
|
||||||
# Hidden imports per pywebview (EdgeChromium su Windows)
|
# Hidden imports per pywebview (EdgeChromium su Windows)
|
||||||
@@ -121,7 +124,7 @@ def run_pyinstaller():
|
|||||||
*add_binaries,
|
*add_binaries,
|
||||||
# Entry point
|
# Entry point
|
||||||
str(ROOT / "main.py"),
|
str(ROOT / "main.py"),
|
||||||
]
|
])
|
||||||
|
|
||||||
log(f"Comando: {' '.join(cmd[:10])}...")
|
log(f"Comando: {' '.join(cmd[:10])}...")
|
||||||
subprocess.run(cmd, check=True, cwd=str(ROOT))
|
subprocess.run(cmd, check=True, cwd=str(ROOT))
|
||||||
|
|||||||
Reference in new issue
Block a user