- Remove Combine import and cancellables - Use NSKeyValueObservation for timeControlStatus (controlled teardown) - Invalidate itemStatusObserver before replacing playerItem - Store endObserver token for proper removal - Add cleanup() method called on view disappear - Proper deinit with direct property cleanup (nonisolated-safe)
218 lines
6.4 KiB
Python
218 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate MiniPlayer app icons (tri-color play button + ring)"""
|
|
|
|
from PIL import Image, ImageDraw
|
|
import math
|
|
import os
|
|
|
|
def create_icon(size):
|
|
"""Create icon at given size"""
|
|
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Background: dark rounded rect
|
|
margin = int(size * 0.05)
|
|
radius = int(size * 0.18)
|
|
draw.rounded_rectangle(
|
|
[margin, margin, size - margin, size - margin],
|
|
radius=radius,
|
|
fill=(28, 28, 30, 255)
|
|
)
|
|
|
|
cx, cy = size / 2, size / 2
|
|
ring_radius = size * 0.38
|
|
ring_width = size * 0.06
|
|
tri_size = size * 0.28
|
|
|
|
# Colors
|
|
RED = (255, 59, 48)
|
|
GREEN = (52, 199, 89)
|
|
BLUE = (0, 122, 255)
|
|
|
|
# === Outer Ring: 3 arc segments ===
|
|
# Red: 150-270°, Green: 270-30°, Blue: 30-150°
|
|
# Draw as thick arcs
|
|
inner_r = ring_radius - ring_width / 2
|
|
outer_r = ring_radius + ring_width / 2
|
|
|
|
def draw_arc_segment(color, start_deg, end_deg):
|
|
"""Draw a thick arc segment"""
|
|
for angle in range(int(start_deg * 10), int(end_deg * 10)):
|
|
a = angle / 10.0
|
|
rad = math.radians(a)
|
|
ix = cx + inner_r * math.cos(rad)
|
|
iy = cy + inner_r * math.sin(rad)
|
|
ox = cx + outer_r * math.cos(rad)
|
|
oy = cy + outer_r * math.sin(rad)
|
|
draw.line([(ix, iy), (ox, oy)], fill=color, width=max(1, int(size * 0.005)))
|
|
|
|
# Ring segments (start from top, clockwise)
|
|
# Red: top-left to bottom-left (180° arc, offset)
|
|
draw_arc_segment(RED, -60, 60) # right side
|
|
draw_arc_segment(GREEN, 60, 180) # bottom
|
|
draw_arc_segment(BLUE, 180, 300) # left/top
|
|
|
|
# === Play Triangle: split into 3 colored sections from center ===
|
|
# Triangle pointing right: vertices relative to center
|
|
# Shift slightly right for visual centering of play button
|
|
offset_x = tri_size * 0.08
|
|
|
|
# Triangle vertices (pointing right)
|
|
top = (cx - tri_size * 0.4 + offset_x, cy - tri_size * 0.55)
|
|
bottom = (cx - tri_size * 0.4 + offset_x, cy + tri_size * 0.55)
|
|
right = (cx + tri_size * 0.55 + offset_x, cy)
|
|
|
|
# Center of triangle
|
|
tcx = (top[0] + bottom[0] + right[0]) / 3
|
|
tcy = (top[1] + bottom[1] + right[1]) / 3
|
|
|
|
# Draw 3 sections from center to each edge
|
|
# Section 1 (Red): center -> top -> right
|
|
draw.polygon([
|
|
(tcx, tcy),
|
|
top,
|
|
right
|
|
], fill=RED)
|
|
|
|
# Section 2 (Green): center -> right -> bottom
|
|
draw.polygon([
|
|
(tcx, tcy),
|
|
right,
|
|
bottom
|
|
], fill=GREEN)
|
|
|
|
# Section 3 (Blue): center -> bottom -> top
|
|
draw.polygon([
|
|
(tcx, tcy),
|
|
bottom,
|
|
top
|
|
], fill=BLUE)
|
|
|
|
# Thin white lines from center to vertices (separators)
|
|
line_w = max(1, int(size * 0.004))
|
|
draw.line([(tcx, tcy), top], fill=(255, 255, 255, 180), width=line_w)
|
|
draw.line([(tcx, tcy), bottom], fill=(255, 255, 255, 180), width=line_w)
|
|
draw.line([(tcx, tcy), right], fill=(255, 255, 255, 180), width=line_w)
|
|
|
|
return img
|
|
|
|
|
|
def generate_macos_icons(output_dir):
|
|
"""Generate macOS .appiconset"""
|
|
appiconset = os.path.join(output_dir, "MiniPlayer.appiconset")
|
|
os.makedirs(appiconset, exist_ok=True)
|
|
|
|
sizes = {
|
|
"icon_16": 16,
|
|
"icon_16@2x": 32,
|
|
"icon_32": 32,
|
|
"icon_32@2x": 64,
|
|
"icon_128": 128,
|
|
"icon_128@2x": 256,
|
|
"icon_256": 256,
|
|
"icon_256@2x": 512,
|
|
"icon_512": 512,
|
|
"icon_512@2x": 1024,
|
|
}
|
|
|
|
images_json = []
|
|
for name, px in sizes.items():
|
|
fname = f"{name}.png"
|
|
icon = create_icon(px)
|
|
icon.save(os.path.join(appiconset, fname))
|
|
|
|
# Parse size and scale
|
|
if "@2x" in name:
|
|
base = name.replace("@2x", "")
|
|
scale = "2x"
|
|
sz = base.replace("icon_", "")
|
|
else:
|
|
scale = "1x"
|
|
sz = name.replace("icon_", "")
|
|
|
|
images_json.append({
|
|
"filename": fname,
|
|
"idiom": "mac",
|
|
"scale": scale,
|
|
"size": f"{sz}x{sz}"
|
|
})
|
|
print(f" {fname}: {px}x{px}")
|
|
|
|
# Write Contents.json
|
|
import json
|
|
contents = {
|
|
"images": images_json,
|
|
"info": {"author": "MiniPlayer", "version": 1}
|
|
}
|
|
with open(os.path.join(appiconset, "Contents.json"), "w") as f:
|
|
json.dump(contents, f, indent=2)
|
|
|
|
print(f" Contents.json written")
|
|
|
|
|
|
def generate_ios_icons(output_dir):
|
|
"""Generate iOS AppIcon set"""
|
|
appiconset = os.path.join(output_dir, "AppIcon.appiconset")
|
|
os.makedirs(appiconset, exist_ok=True)
|
|
|
|
# iOS icon sizes (points x scale = pixels)
|
|
ios_sizes = [
|
|
("20x20", "2x", 40),
|
|
("20x20", "3x", 60),
|
|
("29x29", "2x", 58),
|
|
("29x29", "3x", 87),
|
|
("40x40", "2x", 80),
|
|
("40x40", "3x", 120),
|
|
("60x60", "2x", 120),
|
|
("60x60", "3x", 180),
|
|
("76x76", "2x", 152),
|
|
("83.5x83.5", "2x", 167),
|
|
("1024x1024", "1x", 1024),
|
|
]
|
|
|
|
images_json = []
|
|
for sz, scale, px in ios_sizes:
|
|
fname = f"icon_{px}.png"
|
|
fpath = os.path.join(appiconset, fname)
|
|
if not os.path.exists(fpath):
|
|
icon = create_icon(px)
|
|
icon.save(fpath)
|
|
images_json.append({
|
|
"filename": fname,
|
|
"idiom": "ios-marketing" if px == 1024 else "iphone" if "x20" in sz or "x29" in sz or "x40" in sz or "x60" in sz else "ipad",
|
|
"scale": scale,
|
|
"size": sz
|
|
})
|
|
print(f" {fname}: {px}x{px}")
|
|
|
|
import json
|
|
contents = {
|
|
"images": images_json,
|
|
"info": {"author": "MiniPlayer", "version": 1}
|
|
}
|
|
with open(os.path.join(appiconset, "Contents.json"), "w") as f:
|
|
json.dump(contents, f, indent=2)
|
|
|
|
print(f" Contents.json written")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
base = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
print("=== macOS Icons ===")
|
|
macos_dir = os.path.join(base, "Sources", "Resources", "macOS")
|
|
generate_macos_icons(macos_dir)
|
|
|
|
print("\n=== iOS Icons ===")
|
|
ios_dir = os.path.join(base, "Sources", "Resources", "iOS")
|
|
generate_ios_icons(ios_dir)
|
|
|
|
# Also save a preview
|
|
preview = create_icon(512)
|
|
preview_path = os.path.join(base, "build", "icon_preview.png")
|
|
os.makedirs(os.path.dirname(preview_path), exist_ok=True)
|
|
preview.save(preview_path)
|
|
print(f"\nPreview: {preview_path}")
|
|
|
|
print("\nDone!")
|