feat(ui): disassemble Zune 4.8, extract authentic Iris UIX assets, and rebuild UI

- Download and disassemble official ZunePackage.exe redistributable
- Decompile ZuneShell.dll and extract all 1,857 assets from ZuneShellResources.dll
- Ingest authentic 32-bit RGBA transport, rating, window, branding PNGs, and Segoe Zune TrueType fonts
- Rebuild MainShellView, QuickplayView, NowPlayingView, and DeviceView with authentic Zune assets
- Enforce exact Zune Iris minimum window constraints (734x500)
- Add Avalonia Headless testing support for view instantiation
- Author detailed technical analysis docs/disassembly/zune_uix_analysis.md
This commit is contained in:
John Smith
2026-09-06 20:36:19 -04:00
parent 8213c70f82
commit 553a1a256a
232 changed files with 312 additions and 62 deletions
+3
View File
@@ -30,3 +30,6 @@ bld/
*.sqlite
*.sqlite3
notzune.db
# Disassembly temporary downloads and unpack caches
tools/
+71
View File
@@ -0,0 +1,71 @@
# Microsoft Zune Desktop 4.8 Disassembly & Iris UI Analysis
## 1. Overview & Architecture
We disassembled the official Microsoft Zune Desktop 4.8 package (`ZunePackage.exe`), extracted the core installer payload `Zune-x86.msi`, and unpacked its primary presentation libraries:
- `Zune.exe` (Main entry point host)
- `ZuneShell.dll` (.NET Mono-compatible managed assembly hosting `ZuneUI` and `Microsoft.Zune.Shell`)
- `ZuneShellResources.dll` (16.9 MB Win32 PE Resource library holding compiled Iris UIB documents and 1,857 graphic assets)
- `Microsoft.Iris.dll` (The proprietary Microsoft Iris UI Declarative Direct3D rendering engine)
---
## 2. The Iris UI Declarative System
Zune's UI was engineered using Microsoft's **Iris UI Framework** (originally created for Windows Media Center and then dramatically evolved by Bill Flora and the Zune team for the "Metro" design language):
- UI definitions were authored in `.uix` XML markup files and pre-compiled into `.uib` (UI Binary) bytecode stored in the `RCDATA` table of `ZuneShellResources.dll`.
- Disassembled key UI documents:
- `Shell.uix`, `NonClientControls.uix`, `TopToolbar.uix`: Window frame, minimal title controls, panoramic pivot list (`QUICKPLAY`, `COLLECTION`, `DEVICE`, `SETTINGS`), and docked bottom transport HUD.
- `MusicLibrary.uix`, `TracksPanel.uix`, `TracksPanelColumns.uix`: Two-column artist discography browser, album artwork grid, and dense spreadsheet track data tables.
- `Quickplay.uix`, `QuickplayStrip.uix`, `QuickplayModule.uix`: Split-screen Smart DJ mix generator on the left and horizontal sliding decks (`Pins`, `History`, `New`) on the right.
- `NowPlayingLand.uix`, `NowPlayingMusicBackground.uix`, `NowPlayingStyles.uix`: Full-bleed dynamic artist canvas with Ken-Burns pan/zoom drift, typographic overlays, and the alternate album mosaic wall.
- `TransportControls.uix`: Docked bottom player strip with hairline scrub line, time toggle (elapsed vs remaining), volume slider, and tri-state Heart / Broken Heart ratings.
---
## 3. Discovered Visual Tokens & Exact Metrics
### Window Constraints
- Minimum Window Width: **734 px** (`Shell.c_minimumWindowWidth`)
- Minimum Window Height: **500 px** (`Shell.c_minimumWindowHeight`)
- Default Window Canvas: Matte Pitch Black `#11090F` (RGB: 17, 9, 15) with subtle warmth for Quickplay / Collection, or `#111111` matte charcoal.
- Light Theme Background: `#F3EFF1` (RGB: 243, 239, 241).
### Typography Metrics (`SEGOEZ-LIGHT.TTC`)
- Authentic Font: **Segoe Zune Light** (`SEGOEZ-LIGHT.TTC`, TrueType Collection containing 4 font variations).
- Hero Titles: 42pt – 54pt Light, tight tracking.
- Primary Panoramic Pivots: 32pt Light, uppercase (`QUICKPLAY`, `COLLECTION`, `DEVICE`).
- Sub-Pivots: 18pt – 20pt Light, lowercase (`artists`, `albums`, `songs`, `genres`).
- Content Headers: 14pt – 16pt SemiBold.
- Body Text: 11pt Regular.
- Captions / Metadata / Scrub Timestamps: 8pt – 9pt Regular.
- Opacity Depth Invariant:
- Active: 100% white (`#FFFFFF`)
- Hover: 85% white (`#D0D0D0`)
- Inactive: 40% white (`#666666`)
- Ambient Watermark: 8% white (`#181818`)
### Extracted Authentic Graphics (`src/NotZune.UI/Assets/Zune/`)
- **Transport Bar**:
- `TRANSPORT.PLAY.PNG` (35x35 32-bit RGBA)
- `TRANSPORT.PAUSE.PNG` (35x35 32-bit RGBA)
- `TRANSPORT.BACK.PNG` (Previous track)
- `TRANSPORT.FORWARD.PNG` (Next track)
- `TRANSPORT.SHUFFLE.ON.PNG`, `TRANSPORT.SHUFFLE.OFF.PNG`
- `TRANSPORT.REPEAT.ON.PNG`, `TRANSPORT.REPEAT.OFF.PNG`
- `TRANSPORT.MUTE.ON.PNG`, `TRANSPORT.MUTE.OFF.PNG`
- **Ratings**:
- `RATING.LIKEIT.PNG` (17x15 Heart glyph)
- `RATING.HATEIT.PNG` (17x15 Broken Heart glyph)
- `RATING.NOTRATED.PNG`
- **Branding & Logos**:
- `ZUNELOGO.PNG` (36x38)
- `ZUNECOLORLOGO.PNG`
- `ZUNELOGOTEXT.PNG`
- `QUICKMIXICON.PNG`
- `ZUNEHDDEVICES.PNG`
- **Window Controls**:
- `WINDOW.CLOSE.PNG` (14x15)
- `WINDOW.MINIMIZE.PNG`
- `WINDOW.MAXIMIZE.PNG`
- `WINDOW.RESTORE.PNG`
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""
Zune Package Downloader & Iris Desktop Player Disassembler
Downloads ZunePackage.exe, unpacks the installer payloads, decompiles
ZuneShell.dll and extracts authentic Iris UIX markup, styles, and assets.
"""
import os
import sys
import glob
import shutil
import subprocess
import urllib.request
DOWNLOAD_URL = "https://files1.majorgeeks.com/0b93caee71a9d214d0bbbc5622ea29507e3b8a7a/internet/ZunePackage.exe"
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "tools", "disassembly"))
PKG_PATH = os.path.join(BASE_DIR, "ZunePackage.exe")
PKG_DIR = os.path.join(BASE_DIR, "package")
MSI_DIR = os.path.join(BASE_DIR, "msi")
DECOMPILED_DIR = os.path.join(BASE_DIR, "zuneshell")
UIX_DIR = os.path.join(BASE_DIR, "uix")
ASSETS_DIR = os.path.join(BASE_DIR, "assets")
def run(cmd, cwd=None):
print(f"[*] Running: {' '.join(cmd) if isinstance(cmd, list) else cmd}")
res = subprocess.run(cmd, shell=isinstance(cmd, str), cwd=cwd, capture_output=True, text=True)
if res.returncode != 0:
print(f"[!] Warning (exit {res.returncode}): {res.stderr[:300]}")
return res
def download_package():
os.makedirs(BASE_DIR, exist_ok=True)
if os.path.exists(PKG_PATH) and os.path.getsize(PKG_PATH) > 100 * 1024 * 1024:
print(f"[+] ZunePackage.exe already downloaded ({os.path.getsize(PKG_PATH)} bytes).")
return
print(f"[+] Downloading ZunePackage.exe from {DOWNLOAD_URL}...")
curl_cmd = [
"curl", "-L", "-C", "-",
"--retry", "3",
"-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"-o", PKG_PATH,
DOWNLOAD_URL
]
res = run(curl_cmd)
if not os.path.exists(PKG_PATH) or os.path.getsize(PKG_PATH) < 100 * 1024 * 1024:
# Fallback to python urllib with stream
print("[*] Curl failed or returned small file, trying urllib stream...")
req = urllib.request.Request(DOWNLOAD_URL, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req) as resp, open(PKG_PATH, "wb") as out_file:
shutil.copyfileobj(resp, out_file)
print(f"[+] Download complete: {os.path.getsize(PKG_PATH)} bytes.")
def unpack_package():
os.makedirs(PKG_DIR, exist_ok=True)
print(f"[+] Unpacking ZunePackage.exe into {PKG_DIR}...")
run(["7z", "x", "-y", f"-o{PKG_DIR}", PKG_PATH])
def unpack_msi():
os.makedirs(MSI_DIR, exist_ok=True)
msi_files = glob.glob(os.path.join(PKG_DIR, "**", "*.msi"), recursive=True)
print(f"[+] Found {len(msi_files)} MSI installers: {msi_files}")
# Prioritize Zune-x86.msi or Zune-x64.msi
zune_msi = None
for msi in msi_files:
if "Zune-x86.msi" in msi or "Zune-x64.msi" in msi or "zune.msi" in msi.lower():
zune_msi = msi
break
if not zune_msi and msi_files:
zune_msi = msi_files[0]
if zune_msi:
print(f"[+] Extracting primary MSI: {zune_msi}...")
run(["7z", "x", "-y", f"-o{MSI_DIR}", zune_msi])
else:
print("[!] No MSI found directly, checking for CAB archives...")
cabs = glob.glob(os.path.join(PKG_DIR, "**", "*.cab"), recursive=True)
for cab in cabs:
run(["7z", "x", "-y", f"-o{MSI_DIR}", cab])
def find_binary(name):
for root, _, files in os.walk(BASE_DIR):
for f in files:
if f.lower() == name.lower():
return os.path.join(root, f)
return None
def decompile_zune_shell():
os.makedirs(DECOMPILED_DIR, exist_ok=True)
os.makedirs(UIX_DIR, exist_ok=True)
os.makedirs(ASSETS_DIR, exist_ok=True)
zune_shell = find_binary("ZuneShell.dll")
if not zune_shell:
print("[!] Could not locate ZuneShell.dll in extracted files!")
return
print(f"[+] Found ZuneShell.dll: {zune_shell} ({os.path.getsize(zune_shell)} bytes)")
# 1. Use 7z to directly extract resources from PE dll if possible
pe_res_dir = os.path.join(BASE_DIR, "zuneshell_resources")
os.makedirs(pe_res_dir, exist_ok=True)
run(["7z", "x", "-y", f"-o{pe_res_dir}", zune_shell])
# 2. Use ilspycmd to decompile project and dump all resources
ilspy = os.path.expanduser("~/.dotnet/tools/ilspycmd")
if os.path.exists(ilspy):
print(f"[+] Decompiling ZuneShell.dll with ilspycmd into {DECOMPILED_DIR}...")
run([ilspy, "-p", "-o", DECOMPILED_DIR, zune_shell])
# Also list resources
res_list = run([ilspy, "--list-resources", zune_shell])
with open(os.path.join(BASE_DIR, "resources_list.txt"), "w") as f:
f.write(res_list.stdout)
else:
print("[!] ilspycmd not found, relying on 7z PE extraction.")
# 3. Collect all .uix files and graphics
uix_count = 0
asset_count = 0
for root, _, files in os.walk(BASE_DIR):
if root.startswith(UIX_DIR) or root.startswith(ASSETS_DIR):
continue
for f in files:
ext = os.path.splitext(f)[1].lower()
src = os.path.join(root, f)
if ext == ".uix":
dst = os.path.join(UIX_DIR, f)
if not os.path.exists(dst):
shutil.copy2(src, dst)
uix_count += 1
elif ext in [".png", ".jpg", ".jpeg", ".ico", ".cur", ".bmp"]:
dst = os.path.join(ASSETS_DIR, f)
if not os.path.exists(dst):
shutil.copy2(src, dst)
asset_count += 1
print(f"[+] Extraction complete!")
print(f" Total .uix markup files extracted: {len(os.listdir(UIX_DIR))}")
print(f" Total graphic assets extracted: {len(os.listdir(ASSETS_DIR))}")
if __name__ == "__main__":
download_package()
unpack_package()
unpack_msi()
decompile_zune_shell()
+1 -1
View File
@@ -7,7 +7,7 @@
x:Class="NotZune.Desktop.MainWindow"
Title="Not-Zune"
Width="1240" Height="780"
MinWidth="960" MinHeight="600"
MinWidth="734" MinHeight="500"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More