diff --git a/build.py b/build.py index a506a22..d98eaa8 100644 --- a/build.py +++ b/build.py @@ -15,7 +15,7 @@ '--name=WoWSync', '--onedir' if sys.platform == 'darwin' else '--onefile', '--noconsole', - '--icon=icons/icon.png', + '--icon=icons/icon.icns' if sys.platform == 'darwin' else ('--icon=icons/icon.ico' if sys.platform == 'win32' else '--icon=icons/icon.png'), f'--add-data=icons{";" if sys.platform == "win32" else ":"}icons', '--hidden-import=tkinter', '--hidden-import=sv_ttk', diff --git a/icons/generate_icons.py b/icons/generate_icons.py index 71fe4ee..5144011 100644 --- a/icons/generate_icons.py +++ b/icons/generate_icons.py @@ -3,6 +3,11 @@ from PIL import Image, ImageEnhance from pathlib import Path +try: + from icnsutil import IcnsFile + ICNS_AVAILABLE = True +except ImportError: + ICNS_AVAILABLE = False def make_transparent(image, *, background=(255, 255, 255), alpha_power=1.0, alpha_floor=6): @@ -88,17 +93,19 @@ def main(): print(f" ✓ Saved {original_processed_path}") # Generate icons at different sizes - sizes = { - "icon.png": 256, - "icon_256.png": 256, - "icon_128.png": 128, - "icon_64.png": 64, - "icon_48.png": 48, - "icon_32.png": 32, - "icon_16.png": 16, - } + sizes = [ + ("icon.png", 256), + ("icon_256.png", 256), + ("icon_128.png", 128), + ("icon_64.png", 64), + ("icon_48.png", 48), + ("icon_32.png", 32), + ("icon_16.png", 16), + ] - for filename, size in sizes.items(): + # Process all icon sizes and save as PNG files + icon_sizes_for_ico = {} + for filename, size in sizes: output_path = icons_dir / filename print(f"Generating {filename} ({size}x{size})...") @@ -108,6 +115,76 @@ def main(): # Save as PNG with transparency resized.save(output_path, "PNG", optimize=True) print(f" ✓ Saved {output_path}") + + # Collect images for ICO file (avoid duplicates) + if size not in icon_sizes_for_ico: + icon_sizes_for_ico[size] = resized + + # Generate ICO file for Windows (contains multiple resolutions) + ico_path = icons_dir / "icon.ico" + print(f"\nGenerating icon.ico with multiple resolutions...") + # Sort sizes for consistent ordering in the ICO file + sorted_sizes = sorted(icon_sizes_for_ico.keys()) + + # For ICO files, we need to save with the sizes parameter + # which tells Pillow to create an ICO with multiple embedded images + if sorted_sizes: + # Use the original transparent image and let Pillow resize to each size + transparent_img.save(ico_path, format='ICO', + sizes=[(s, s) for s in sorted_sizes]) + print(f" ✓ Saved {ico_path} with {len(sorted_sizes)} resolutions: {sorted_sizes}") + + # Generate ICNS file for macOS (contains multiple resolutions) + if ICNS_AVAILABLE: + icns_path = icons_dir / "icon.icns" + print(f"\nGenerating icon.icns with multiple resolutions...") + + # Create ICNS file + icns = IcnsFile() + + # macOS ICNS standard sizes - use the naming convention icnsutil expects + # Format: icon_x.png and icon_x@2x.png for retina + # Note: 64x64 is covered by 32x32@2x + icns_mappings = [ + (16, 32), # 16x16 and 16x16@2x (32x32) + (32, 64), # 32x32 and 32x32@2x (64x64) + (128, 256), # 128x128 and 128x128@2x (256x256) + (256, 512), # 256x256 and 256x256@2x (512x512) + (512, 1024), # 512x512 and 512x512@2x (1024x1024) + ] + + for normal_size, retina_size in icns_mappings: + # Add normal resolution + if normal_size in icon_sizes_for_ico: + img = icon_sizes_for_ico[normal_size] + else: + img = transparent_img.resize((normal_size, normal_size), Image.Resampling.LANCZOS) + + temp_png = icons_dir / f"icon_{normal_size}x{normal_size}.png" + img.save(temp_png, "PNG") + icns.add_media(file=str(temp_png)) + temp_png.unlink() + + # Add retina resolution + if retina_size in icon_sizes_for_ico: + retina_img = icon_sizes_for_ico[retina_size] + elif retina_size <= 1024: + retina_img = transparent_img.resize((retina_size, retina_size), Image.Resampling.LANCZOS) + else: + # For 1024, just use the processed image + retina_img = transparent_img.resize((1024, 1024), Image.Resampling.LANCZOS) + + temp_retina_png = icons_dir / f"icon_{normal_size}x{normal_size}@2x.png" + retina_img.save(temp_retina_png, "PNG") + icns.add_media(file=str(temp_retina_png)) + temp_retina_png.unlink() + + # Write the ICNS file + icns.write(str(icns_path)) + print(f" ✓ Saved {icns_path} with standard macOS resolutions") + else: + print(f"\n⚠ Skipping icon.icns generation (icnsutil not available)") + print(f" Install with: pip install icnsutil") print("\nAll icons generated successfully!") diff --git a/icons/icon.icns b/icons/icon.icns new file mode 100644 index 0000000..0466571 Binary files /dev/null and b/icons/icon.icns differ diff --git a/icons/icon.ico b/icons/icon.ico new file mode 100644 index 0000000..1df0309 Binary files /dev/null and b/icons/icon.ico differ diff --git a/requirements.txt b/requirements.txt index 4a0c1ce..c2c2533 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ psutil==7.2.0 dbus-fast==3.1.2 async-tkinter-loop==0.10.3 # Build tools (optional, for packaging) -pyinstaller==6.17.0; extra == "build" \ No newline at end of file +pyinstaller==6.17.0; extra == "build" +icnsutil==1.1.0; extra == "build" \ No newline at end of file diff --git a/wow_sync/tray/tray_macos.py b/wow_sync/tray/tray_macos.py index e0f9339..5462379 100644 --- a/wow_sync/tray/tray_macos.py +++ b/wow_sync/tray/tray_macos.py @@ -178,7 +178,11 @@ def __init__(self, on_show: Optional[Callable] = None, async def setup(self): """Setup the tray icon using macOS NSStatusBar.""" - icon_path = ICON_DIR / "icon.png" + # Use ICNS file for macOS (supports multiple resolutions and retina displays) + icon_path = ICON_DIR / "icon.icns" + # Fallback to PNG if ICNS not available + if not icon_path.exists(): + icon_path = ICON_DIR / "icon.png" if not icon_path.exists(): raise RuntimeError(f"Icon not found: {icon_path}") diff --git a/wow_sync/tray/tray_windows.py b/wow_sync/tray/tray_windows.py index c4d434f..0e7b840 100644 --- a/wow_sync/tray/tray_windows.py +++ b/wow_sync/tray/tray_windows.py @@ -49,7 +49,8 @@ def __init__(self, on_show: Optional[Callable] = None, async def setup(self): """Setup the tray icon using Windows Shell NotifyIcon.""" - icon_path = ICON_DIR / "icon.png" + # Use ICO file for Windows (supports multiple resolutions) + icon_path = ICON_DIR / "icon.ico" if not icon_path.exists(): raise RuntimeError(f"Icon not found: {icon_path}")