From 0f1f2221235caab7f7d3ede783c24373c7ed3dd1 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 07 Jun 2026 02:11:31 +0800
Subject: [PATCH] release: add App Store marketing screenshots
---
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/03-multi-tag-system-2880x1800.png | 0
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/02-four-views-2880x1800.png | 0
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/04-quick-search-2880x1800.png | 0
Release/AppStore-7.8.15-20260605.2337/Screenshots/render_marketing_screenshots.py | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/06-private-offline-2880x1800.png | 0
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/01-hero-launcher-2880x1800.png | 0
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/_contact-sheet.png | 0
Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/05-app-notes-2880x1800.png | 0
8 files changed, 277 insertions(+), 0 deletions(-)
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/01-hero-launcher-2880x1800.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/01-hero-launcher-2880x1800.png
new file mode 100644
index 0000000..6abe0f7
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/01-hero-launcher-2880x1800.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/02-four-views-2880x1800.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/02-four-views-2880x1800.png
new file mode 100644
index 0000000..8622cb9
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/02-four-views-2880x1800.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/03-multi-tag-system-2880x1800.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/03-multi-tag-system-2880x1800.png
new file mode 100644
index 0000000..abb4564
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/03-multi-tag-system-2880x1800.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/04-quick-search-2880x1800.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/04-quick-search-2880x1800.png
new file mode 100644
index 0000000..49425c6
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/04-quick-search-2880x1800.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/05-app-notes-2880x1800.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/05-app-notes-2880x1800.png
new file mode 100644
index 0000000..272e345
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/05-app-notes-2880x1800.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/06-private-offline-2880x1800.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/06-private-offline-2880x1800.png
new file mode 100644
index 0000000..0d159c1
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/06-private-offline-2880x1800.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/_contact-sheet.png b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/_contact-sheet.png
new file mode 100644
index 0000000..7d691a9
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/AppStore-Marketing/en/_contact-sheet.png
Binary files differ
diff --git a/Release/AppStore-7.8.15-20260605.2337/Screenshots/render_marketing_screenshots.py b/Release/AppStore-7.8.15-20260605.2337/Screenshots/render_marketing_screenshots.py
new file mode 100644
index 0000000..e75be6e
--- /dev/null
+++ b/Release/AppStore-7.8.15-20260605.2337/Screenshots/render_marketing_screenshots.py
@@ -0,0 +1,277 @@
+from pathlib import Path
+from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps
+
+
+W, H = 2880, 1800
+ROOT = Path("/Users/ar/Projects/Taglauncher")
+ASSETS = ROOT / "docs" / "assets"
+RELEASE = Path("/Users/ar/Projects/Taglauncher-7.8.15-source/Release/AppStore-7.8.15-20260605.2337")
+OUT = RELEASE / "Screenshots" / "AppStore-Marketing" / "en"
+
+FONT_REG = "/System/Library/Fonts/SFNS.ttf"
+FONT_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
+FONT_BLACK = "/System/Library/Fonts/Supplemental/Arial Black.ttf"
+FONT_MONO = "/System/Library/Fonts/SFNSMono.ttf"
+
+
+def f(size, bold=False, black=False, mono=False):
+ path = FONT_MONO if mono else FONT_BLACK if black else FONT_BOLD if bold else FONT_REG
+ return ImageFont.truetype(path, size)
+
+
+def rgba(c):
+ if len(c) == 4:
+ return c
+ return (*c, 255)
+
+
+def lerp(a, b, t):
+ return int(a + (b - a) * t)
+
+
+def gradient(seed=0):
+ palettes = [
+ ((248, 251, 255), (243, 248, 246), [(90, 150, 255), (92, 210, 150), (255, 118, 168)]),
+ ((250, 252, 255), (247, 246, 255), [(107, 142, 255), (255, 137, 182), (112, 215, 160)]),
+ ((250, 252, 249), (245, 250, 255), [(96, 215, 151), (104, 164, 255), (255, 174, 92)]),
+ ((251, 252, 255), (246, 250, 248), [(94, 175, 255), (255, 128, 160), (124, 214, 132)]),
+ ((252, 251, 255), (247, 251, 252), [(115, 142, 255), (104, 219, 177), (255, 199, 92)]),
+ ((250, 252, 255), (246, 249, 246), [(80, 210, 150), (120, 155, 255), (255, 130, 150)]),
+ ]
+ top, bottom, accents = palettes[seed % len(palettes)]
+ small = Image.new("RGBA", (360, 225), top + (255,))
+ pix = small.load()
+ for y in range(small.height):
+ t = y / (small.height - 1)
+ for x in range(small.width):
+ side = x / (small.width - 1)
+ c = tuple(lerp(top[i], bottom[i], t * 0.85 + side * 0.15) for i in range(3))
+ pix[x, y] = (*c, 255)
+ base = small.resize((W, H), Image.Resampling.BICUBIC)
+ spots = [
+ (-260, -180, 1050, accents[0], 44),
+ (W - 720, 100, 1100, accents[1], 36),
+ (W // 2 - 360, H - 560, 980, accents[2], 30),
+ ]
+ for x, y, size, color, alpha in spots:
+ layer = Image.new("RGBA", (W, H), (0, 0, 0, 0))
+ d = ImageDraw.Draw(layer)
+ d.ellipse((x, y, x + size, y + size), fill=(*color, alpha))
+ layer = layer.filter(ImageFilter.GaussianBlur(size // 4))
+ base = Image.alpha_composite(base, layer)
+ return base
+
+
+def shadow(base, xywh, radius=50, blur=54, offset=(0, 34), alpha=48):
+ x, y, w, h = xywh
+ layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
+ m = Image.new("L", base.size, 0)
+ d = ImageDraw.Draw(m)
+ ox, oy = offset
+ d.rounded_rectangle((x + ox, y + oy, x + w + ox, y + h + oy), radius, fill=alpha)
+ m = m.filter(ImageFilter.GaussianBlur(blur))
+ layer.paste((28, 38, 58, 255), (0, 0), m)
+ return Image.alpha_composite(base, layer)
+
+
+def round_mask(size, radius):
+ m = Image.new("L", size, 0)
+ d = ImageDraw.Draw(m)
+ d.rounded_rectangle((0, 0, size[0], size[1]), radius, fill=255)
+ return m
+
+
+def crop_alpha(im):
+ im = im.convert("RGBA")
+ if im.getbands()[-1] == "A":
+ box = im.split()[-1].getbbox()
+ if box:
+ return im.crop(box)
+ return im
+
+
+def fit_image(im, size, mode="contain"):
+ im = im.convert("RGBA")
+ iw, ih = im.size
+ w, h = size
+ if mode == "cover":
+ scale = max(w / iw, h / ih)
+ else:
+ scale = min(w / iw, h / ih)
+ nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale))
+ resized = im.resize((nw, nh), Image.Resampling.LANCZOS)
+ if mode == "cover":
+ left = max(0, (nw - w) // 2)
+ top = max(0, (nh - h) // 2)
+ return resized.crop((left, top, left + w, top + h))
+ out = Image.new("RGBA", size, (255, 255, 255, 0))
+ out.alpha_composite(resized, ((w - nw) // 2, (h - nh) // 2))
+ return out
+
+
+def card(base, image, xywh, radius=46, mode="contain", pad=0, bg=(255, 255, 255, 222), border=(255, 255, 255, 160)):
+ x, y, w, h = xywh
+ base = shadow(base, xywh, radius=radius)
+ panel = Image.new("RGBA", (w, h), bg)
+ pd = ImageDraw.Draw(panel)
+ pd.rounded_rectangle((0, 0, w - 1, h - 1), radius, fill=bg, outline=border, width=2)
+ inner = fit_image(image, (w - pad * 2, h - pad * 2), mode=mode)
+ panel.alpha_composite(inner, (pad, pad))
+ mask = round_mask((w, h), radius)
+ clipped = Image.new("RGBA", (w, h), (0, 0, 0, 0))
+ clipped.paste(panel, (0, 0), mask)
+ base.alpha_composite(clipped, (x, y))
+ return base
+
+
+def text(draw, xy, body, size, fill=(16, 18, 24), bold=False, black=False, spacing=18, mono=False):
+ draw.multiline_text(xy, body, font=f(size, bold=bold, black=black, mono=mono), fill=fill, spacing=spacing)
+
+
+def small_brand(draw, x, y, dark=False):
+ fill = (24, 28, 36) if not dark else (255, 255, 255)
+ draw.rounded_rectangle((x, y, x + 270, y + 82), 41, fill=(255, 255, 255, 180), outline=(255, 255, 255, 180), width=2)
+ icon = Image.open(ASSETS / "app-icon-256.png").convert("RGBA").resize((52, 52), Image.Resampling.LANCZOS)
+ return icon, fill
+
+
+def paste_brand(base, x=132, y=118):
+ icon, fill = small_brand(ImageDraw.Draw(base), x, y)
+ base.alpha_composite(icon, (x + 20, y + 15))
+ ImageDraw.Draw(base).text((x + 88, y + 23), "TagLauncher", font=f(28, bold=True), fill=fill)
+
+
+def save(base, name):
+ OUT.mkdir(parents=True, exist_ok=True)
+ path = OUT / name
+ base.convert("RGB").save(path, quality=96)
+ print(path)
+
+
+def slide_hero():
+ base = gradient(1)
+ paste_brand(base)
+ d = ImageDraw.Draw(base)
+ text(d, (132, 430), "Possibly the best\nMac app launcher\nin the world", 128, black=True, spacing=34)
+ text(d, (140, 1145), "One shortcut. Every app.\nOrganized by tags.", 54, fill=(38, 43, 53), spacing=28)
+ hero = Image.open(ASSETS / "hero-visual.jpg")
+ base = card(base, hero, (1370, 168, 1330, 1200), radius=38, mode="contain", pad=0, bg=(255, 255, 255, 145))
+ d = ImageDraw.Draw(base)
+ d.rounded_rectangle((140, 1430, 680, 1536), 53, fill=(255, 255, 255, 186), outline=(255, 255, 255, 210), width=2)
+ text(d, (190, 1460), "macOS 15.0+ | 29 languages", 34, fill=(70, 76, 88), bold=True)
+ save(base, "01-hero-launcher-2880x1800.png")
+
+
+def slide_views():
+ base = gradient(0)
+ paste_brand(base)
+ d = ImageDraw.Draw(base)
+ text(d, (1510, 250), "Four views.\nEverything on\none screen.", 112, black=True, spacing=30)
+ text(d, (1518, 790), "Flat, plain containers,\ncolored containers, and grid.", 48, fill=(76, 82, 96), spacing=20)
+ modes = ["Flat", "Plain containers", "Colored containers", "Plain grid"]
+ y = 1080
+ for i, m in enumerate(modes):
+ d.ellipse((1524, y + i * 95 + 18, 1552, y + i * 95 + 46), fill=(0, 117, 255))
+ text(d, (1588, y + i * 95), m, 46, fill=(31, 35, 45), bold=True)
+ imgs = [Image.open(ASSETS / "views" / f"view-0{i}.jpg") for i in range(1, 5)]
+ positions = [(150, 300), (800, 300), (150, 880), (800, 880)]
+ for im, (x, y) in zip(imgs, positions):
+ base = card(base, im, (x, y, 590, 384), radius=30, mode="cover", pad=0, bg=(255, 255, 255, 210))
+ save(base, "02-four-views-2880x1800.png")
+
+
+def slide_tags():
+ base = gradient(2)
+ paste_brand(base)
+ d = ImageDraw.Draw(base)
+ text(d, (132, 285), "One app.\nMany tags.", 128, black=True, spacing=34)
+ text(d, (140, 740), "Stop forcing tools into\nsingle folders. Put an app\nwherever it belongs.", 50, fill=(58, 64, 76), spacing=24)
+ for i, label in enumerate(["Reading", "Coding", "AI", "Design"]):
+ x = 140 + i * 210
+ y = 1180 + (i % 2) * 98
+ colors = [(88, 197, 99), (177, 101, 207), (83, 160, 238), (255, 99, 93)]
+ d.rounded_rectangle((x, y, x + 178, y + 62), 31, fill=colors[i])
+ d.text((x + 28, y + 16), label, font=f(24, bold=True), fill=(255, 255, 255))
+ shot = Image.open(ASSETS / "tags-management.jpg")
+ base = card(base, shot, (1160, 250, 1500, 975), radius=42, mode="cover", pad=0, bg=(255, 255, 255, 210))
+ save(base, "03-multi-tag-system-2880x1800.png")
+
+
+def slide_search():
+ base = gradient(3)
+ paste_brand(base)
+ d = ImageDraw.Draw(base)
+ text(d, (136, 280), "Search apps.\nSearch tags.\nSearch notes.", 104, black=True, spacing=28)
+ text(d, (144, 815), "Quick Search finds the right app\neven when you remember only\na tag or note.", 44, fill=(62, 68, 82), spacing=22)
+ d.rounded_rectangle((144, 1235, 680, 1345), 55, fill=(255, 255, 255, 190), outline=(255, 255, 255, 220), width=2)
+ text(d, (194, 1262), "Fn + Space", 38, fill=(20, 24, 32), bold=True, mono=True)
+ qs = crop_alpha(Image.open(ASSETS / "quick-search.png"))
+ base = card(base, qs, (1180, 205, 1440, 1160), radius=70, mode="contain", pad=62, bg=(255, 255, 255, 214))
+ save(base, "04-quick-search-2880x1800.png")
+
+
+def slide_notes():
+ base = gradient(4)
+ paste_brand(base)
+ d = ImageDraw.Draw(base)
+ text(d, (132, 250), "App notes.\nFind them\nfast.", 104, black=True, spacing=28)
+ text(d, (140, 765), "Remember what each tool is for.\nQuick Search can find notes too.", 44, fill=(58, 64, 76), spacing=22)
+ memo = Image.open(ASSETS / "app-memo.png")
+ base = card(base, memo, (990, 230, 1570, 1170), radius=46, mode="cover", pad=0, bg=(255, 255, 255, 210))
+ d = ImageDraw.Draw(base)
+ d.rounded_rectangle((150, 1220, 610, 1326), 53, fill=(255, 255, 255, 185), outline=(255, 255, 255, 215), width=2)
+ text(d, (195, 1248), "Hover. Read. Launch.", 34, fill=(35, 39, 50), bold=True)
+ save(base, "05-app-notes-2880x1800.png")
+
+
+def slide_privacy():
+ base = gradient(5)
+ paste_brand(base)
+ d = ImageDraw.Draw(base)
+ text(d, (132, 285), "100% offline.\n100% ad-free.", 126, black=True, spacing=36)
+ text(d, (140, 770), "Your tags, notes, and layout\nstay on your Mac. No analytics,\nno ads, no network dependency.", 48, fill=(58, 64, 76), spacing=24)
+ shield = Image.open(ASSETS / "shield-check.png").convert("RGBA")
+ backup = Image.open(ASSETS / "backup-restore.jpg")
+ settings = Image.open(ASSETS / "screenshot-settings.jpg")
+ base = card(base, backup, (1120, 255, 1360, 745), radius=46, mode="cover", pad=0, bg=(255, 255, 255, 210))
+ base = card(base, settings, (1540, 860, 900, 565), radius=42, mode="cover", pad=0, bg=(255, 255, 255, 210))
+ shield = shield.resize((430, 430), Image.Resampling.LANCZOS)
+ base.alpha_composite(shield, (2240, 180))
+ d = ImageDraw.Draw(base)
+ for i, item in enumerate(["Local data", "JSON backup", "No ads"]):
+ y = 1190 + i * 88
+ d.rounded_rectangle((150, y, 560, y + 64), 32, fill=(255, 255, 255, 185), outline=(255, 255, 255, 215), width=2)
+ text(d, (190, y + 15), item, 26, fill=(42, 48, 60), bold=True)
+ save(base, "06-private-offline-2880x1800.png")
+
+
+def contact_sheet():
+ files = sorted(OUT.glob("*.png"))
+ thumbs = []
+ for p in files:
+ im = Image.open(p).resize((720, 450), Image.Resampling.LANCZOS)
+ thumbs.append((p, im))
+ sheet = Image.new("RGB", (1500, 1450), (246, 248, 252))
+ d = ImageDraw.Draw(sheet)
+ for idx, (p, im) in enumerate(thumbs):
+ x = 40 + (idx % 2) * 740
+ y = 40 + (idx // 2) * 470
+ sheet.paste(im, (x, y))
+ d.text((x, y + 455), p.name, font=f(18, bold=True), fill=(48, 54, 68))
+ path = OUT / "_contact-sheet.png"
+ sheet.save(path, quality=94)
+ print(path)
+
+
+def main():
+ slide_hero()
+ slide_views()
+ slide_tags()
+ slide_search()
+ slide_notes()
+ slide_privacy()
+ contact_sheet()
+
+
+if __name__ == "__main__":
+ main()
--
Gitblit v1.9.3